diff --git a/CHANGELOG.md b/CHANGELOG.md index 64c4b2f..eb6609e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,50 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v0.5.0] - 2024-06-24 + +The release incorporates the up-to-date [API contract](openAPIDefinition.json) as of 2024-06-24 22:03:00 GMT. + +### Added + +- Added support of the following ENUMs as type aliases: + - The type `BillingSubscriptionType` as the attribute of the response of the method `GetCurrentUserInfo`. + - The type `BranchState` as attributes of the response of the method `GetProjectBranch`. + - The type `ConsumptionHistoryGranularity` as the argument of the method `GetConsumptionHistoryPerAccount` and `GetConsumptionHistoryPerProject`. + - The types `EndpointPoolerMode`, `EndpointState` as the attributes of the struct `Endpoint`, which defines the + response of the method `GetProjectEndpoint`. + - The type `EndpointType` as the attribute which defines the endpoint's type to create an endpoint, or define the + options of the branch's endpoints. + - The type `IdentityProviderId` as the attribute of the struct `CurrentUserAuthAccount` which defines the response + of the method `GetCurrentUserInfo`. + - The types `OperationAction` and `OperationStatus` as the attributes of the struct `Operation` which defines the + response of several endpoints which include the operations. + - The type `Provisioner` which defines the Neon compute provisioner's type. +- Added the method `GetProjectBranchSchema` to retrieve the database schema, see details [here](https://api-docs.neon.tech/reference/getprojectbranchschema). +- Added the methods to retrieve the consumption metrics: + - `GetConsumptionHistoryPerAccount` allows to read the account's consumption history, see details [here](https://api-docs.neon.tech/reference/getconsumptionhistoryperaccount). + - `GetConsumptionHistoryPerProject` allows to read the consumption history for a list of projects, see details [here](https://api-docs.neon.tech/reference/getconsumptionhistoryperproject). +- Added the method `GetCurrentUserOrganizations` to read all organization which a given user belongs to. +- Added support of the organization ID (`orgID` argument) when using the following methods: + - `ListProjectsConsumption`, see details [here](https://api-docs.neon.tech/reference/listprojectsconsumption). +- Added the name, the address and the tax information to the billing details of the account: `BillingAccount` struct. + +### Changed + +- All arguments which end with the suffices Id/Ids, Url/Urls, Uri/Uris will follow the Go convention. For example, + the query parameter `project_ids` will correspond to the method's argument `projectIDs`. + +### Deprecated + +- The method `SetPrimaryProjectBranch` is deprecated, please use the method `SetDefaultProjectBranch` instead. +- The label "primary" branch and the attributes `Primary` is deprecated for the label "default" and the respective + attribute `Default`. See the struct `Branch` for example. +- The attribute `ProxyHost` of the struct `Endpoint` is deprecated, please use the attribute `Host` instead. +- The attribute `CpuUsedSec` of the structs `Project` and `ProjectListItem` is deprecated, + please use the attribute `ComputeTimeSeconds` instead. +- The attribute `QuotaResetAt` of the structs `Project` and `ProjectListItem` is deprecated, + please use the attribute `ConsumptionPeriodEnd` instead. + ## [v0.4.9] - 2024-04-13 The release incorporates the up-to-date [API contract](openAPIDefinition.json) as of 2024-04-13 11:00:00 GMT. diff --git a/generator/generator.go b/generator/generator.go index 6077cba..3500c33 100644 --- a/generator/generator.go +++ b/generator/generator.go @@ -1,56 +1,34 @@ package generator import ( - "bytes" "embed" "encoding/json" "errors" "fmt" - "go/format" "io" - "io/fs" "log" "net/http" "os" "os/exec" + "path" + "slices" "sort" + "strconv" "strings" "text/template" "github.com/getkin/kin-openapi/openapi3" ) +//go:embed templates +var templatesFS embed.FS + var ( - //go:embed templates - templates embed.FS - templateGen map[string]*template.Template + templateNameSDK = []string{"sdk.go.templ", "sdk_test.go.templ"} + templateNameMock = []string{"mockhttp.go.templ", "mockhttp_test.go.templ"} + templateNameStatic = []string{"go.mod.templ", "doc.go.templ", "error.go.templ"} ) -// read and parse templates -func init() { - tmplFiles, err := fs.ReadDir(templates, "templates") - if err != nil { - panic(err) - } - - if templateGen == nil { - templateGen = make(map[string]*template.Template, len(tmplFiles)) - } - - for _, tmpl := range tmplFiles { - if tmpl.IsDir() { - continue - } - - pt, err := template.ParseFS(templates, "templates/"+tmpl.Name()) - if err != nil { - panic(err) - } - - templateGen[tmpl.Name()] = pt - } -} - // Config generator configurations. type Config struct { // OpenAPIReader defines the OpenAPI specs input. @@ -62,6 +40,8 @@ type Config struct { // Run executes code generation using the OpenAPI spec. func Run(cfg Config) error { + templates := template.Must(template.ParseFS(templatesFS, "templates/*")) + specBytes, err := io.ReadAll(cfg.OpenAPIReader) if err != nil { return errors.New("cannot read OpenAPI spec: " + err.Error()) @@ -76,49 +56,68 @@ func Run(cfg Config) error { if err != nil { return errors.New("cannot extract ordered list of endpoints from the OpenAPI spec: " + err.Error()) } - tempInput := extractSpecs(spec, orderedEndpointRoutes) + tempInputSDK, tempInputMock := extractSpecs(spec, orderedEndpointRoutes) - var f io.WriteCloser - defer func() { _ = f.Close() }() + if err := generateFiles(templates, templateNameSDK, tempInputSDK, cfg.PathOutput); err != nil { + return fmt.Errorf("could not generate sdk files: %w", err) + } - for fName, temp := range templateGen { - fName = strings.Replace(fName, ".templ", "", -1) - if f, err = os.Create(cfg.PathOutput + "/" + fName); err != nil { - return err - } + if err := generateFiles(templates, templateNameMock, tempInputMock, cfg.PathOutput); err != nil { + return fmt.Errorf("could not generate mock files: %w", err) + } - var buf bytes.Buffer - if err := temp.Execute(&buf, &tempInput); err != nil { - return err - } + if err := generateFiles(templates, templateNameStatic, nil, cfg.PathOutput); err != nil { + return fmt.Errorf("could not generate static files: %w", err) + } - o := buf.Bytes() - if strings.HasSuffix(fName, ".go") { - o, err = format.Source(buf.Bytes()) - if err != nil { - return err - } - } + var e error + if v, _ := strconv.ParseBool(os.Getenv("SKIP_TEST")); !v { + e = testGeneratedCode(cfg.PathOutput) + } - if _, err := f.Write(o); err != nil { - return err - } + if v, _ := strconv.ParseBool(os.Getenv("SKIP_FORMATTING")); !v { + e = errors.Join(e, formatGeneratedCode(cfg.PathOutput)) } - if os.Getenv("SKIP_TEST") == "1" { - return nil + return e +} + +func formatGeneratedCode(p string) error { + cmd := exec.Command("go", "fmt", ".") + cmd.Dir = p + if err := cmd.Start(); err != nil { + panic(err) + } + if err := cmd.Wait(); err != nil { + return fmt.Errorf("failed to run go fmt: %w", err) } + return nil +} - return testGeneratedCode(cfg.PathOutput) +func generateFiles(t *template.Template, templateNames []string, data any, p string) error { + for _, templateName := range templateNames { + outputFileName := strings.TrimSuffix(templateName, ".templ") + filePath := path.Join(p, outputFileName) + f, err := os.Create(filePath) + defer func() { _ = f.Close() }() + if err != nil { + return fmt.Errorf("could not open file: %s. %w", filePath, err) + } + + if err := t.ExecuteTemplate(f, templateName, data); err != nil { + return fmt.Errorf("could not generate a file %s. %w", filePath, err) + } + } + return nil } -func extractSpecs(spec openAPISpec, orderedEndpointRoutes []string) templateInput { +func extractSpecs(spec openAPISpec, orderedEndpointRoutes []string) (templateInputSDK, templateInputMock) { if len(spec.Servers) < 1 { panic("no server spec found") } - endpoints := generateEndpointsImplementationMethods(spec, orderedEndpointRoutes) m := generateModels(spec) + endpoints := generateEndpointsImplementationMethods(spec, orderedEndpointRoutes) endpointsStr := make([]string, len(endpoints)) endpointsTestStr := make([]string, 0, len(endpoints)) @@ -475,16 +474,24 @@ func extractSpecs(spec openAPISpec, orderedEndpointRoutes []string) templateInpu } filterModels(m, models, *s.RequestBodyStruct) } - } - return templateInput{ - Info: spec.Info.Description, - ServerURL: spec.Servers[0].URL, - EndpointsImplementation: endpointsStr, - EndpointsImplementationTest: endpointsTestStr, - EndpointsResponseExample: mockResponses, - Types: models.generateCode(), + // filter models based on the parameters + for _, param := range slices.Concat(s.RequestParametersPath, s.RequestParametersQuery) { + mm, ok := m[param.v] + if ok { + filterModels(m, models, mm) + } + } } + + return templateInputSDK{ + ServerURL: spec.Servers[0].URL, + EndpointsImplementation: endpointsStr, + Types: models.generateCode(), + EndpointsImplementationTest: endpointsTestStr, + }, templateInputMock{ + EndpointsResponseExample: mockResponses, + } } func skipTest(route string) bool { @@ -522,7 +529,7 @@ func testGeneratedCode(p string) error { panic(err) } if err := cmd.Wait(); err != nil { - return errors.New("failed test") + return fmt.Errorf("failed test: %w", err) } return nil } @@ -531,13 +538,15 @@ type openAPISpec struct { openapi3.T } -type templateInput struct { - Info string +type templateInputSDK struct { ServerURL string EndpointsImplementation []string - EndpointsImplementationTest []string Types []string - EndpointsResponseExample map[string]map[string]mockResponse + EndpointsImplementationTest []string +} + +type templateInputMock struct { + EndpointsResponseExample map[string]map[string]mockResponse } type endpointImplementation struct { @@ -732,7 +741,7 @@ func (e endpointImplementation) generateMethodImplementationTest() string { prf := "\t\t" + v.canonicalName() o += fmt.Sprintf("%s %v\n", prf, v.argType(!v.required)) dummyDate := v.generateDummy() - if !v.required { + if !v.required && !v.isArray { dummyDate = wrapIntoPointerGenFn(dummyDate) } testInpt += fmt.Sprintf("\t\t%s: %v,\n", prf, dummyDate) @@ -831,7 +840,7 @@ func (e endpointImplementation) generateMethodImplementationTestNoResponseExpect prf := "\t\t" + v.canonicalName() o += fmt.Sprintf("%s %v\n", prf, v.argType(!v.required)) dummyDate := v.generateDummy() - if !v.required { + if !v.required && !v.isArray { dummyDate = wrapIntoPointerGenFn(dummyDate) } testInpt += fmt.Sprintf("\t\t%s: %v,\n", prf, dummyDate) @@ -927,10 +936,37 @@ func (e endpointImplementation) generateQueryBuilder() string { } for _, p := range filterOptionalParameters(e.RequestParametersQuery) { - optionalCondition := p.canonicalName() + " != nil " - ifStatement := "\tif " + optionalCondition + "{\n" - queryElement := `"` + p.name() + "=\"+" + p.routeElement(true) - o += ifStatement + "\t\tqueryElements = append(queryElements, " + queryElement + ")\n" + var ( + ifStatement string + queryElement string + tmpArrayDef string + ) + + switch p.isArray { + case false: + optionalCondition := p.canonicalName() + " != nil " + ifStatement = "\tif " + optionalCondition + "{\n" + queryElement = `"` + p.name() + "=\"+" + p.routeElement(true) + + case true: + optionalCondition := "len(" + p.canonicalName() + ") > 0 " + ifStatement = "\tif " + optionalCondition + "{\n" + + switch p.v { + case "string": + queryElement = `"` + p.name() + "=\"+strings.Join(" + p.canonicalName() + `, ",")` + + default: + tmpArrName := p.canonicalName() + "Tmp" + tmpArrayDef = "\t\tvar " + tmpArrName + " = make([]string, len(" + p.canonicalName() + "))\n" + tmpArrayDef += "\t\tfor i, el := range " + p.canonicalName() + " {\n" + tmpArrayDef += "\t\t\t" + tmpArrName + "[i] = fmt.Sprintf(\"%v\", el)\n" + tmpArrayDef += "\t\t}\n" + queryElement = `"` + p.name() + "=\"+strings.Join(" + tmpArrName + `, ",")` + } + } + + o += ifStatement + tmpArrayDef + "\t\tqueryElements = append(queryElements, " + queryElement + ")\n" o += "\t}\n" } @@ -1009,6 +1045,7 @@ func (v fieldType) argType() string { type field struct { k, v, format string description string + isArray bool required bool isInPath bool isInQuery bool @@ -1063,6 +1100,8 @@ func correctSpecialTagWords(s string) string { switch sUp := strings.ToUpper(s); sUp { case "ID", "URI", "URL": return sUp + case "IDS", "URIS", "URLS": + return sUp[:len(sUp)-1] + strings.ToLower(sUp[len(sUp)-1:]) default: return s } @@ -1082,18 +1121,16 @@ func objNameGoConventionExport(s string) string { } func (v field) routeElement(withPointer ...bool) string { - r := v.canonicalName() + base := func() string { + r := v.canonicalName() - switch v.format { - case "date-time", "date": - return r + ".Format(time.RFC3339)" - - default: - if len(withPointer) > 0 && withPointer[0] { + if len(withPointer) > 0 && withPointer[0] && v.format != "date-time" && v.format != "date" { r = "*" + r } switch v.format { + case "date-time", "date": + return r + ".Format(time.RFC3339)" case "int64": return "strconv.FormatInt(" + r + ", 10)" case "int32": @@ -1103,54 +1140,81 @@ func (v field) routeElement(withPointer ...bool) string { case "float": return "strconv.FormatFloat(" + r + ", 'f', -1, 32)" default: - switch v.v { - case "integer": + switch { + case v.v == "integer": return "strconv.FormatInt(int64(" + r + "), 10)" - case "boolean": + case v.v == "boolean": varName := v.canonicalName() return "func (" + varName + ` bool) string { if ` + varName + ` { return "true" }; return "false" } (` + r + ")" + // enum case + case startsWithNumber(v.v) || startsWithCapitalLetter(v.v): + return "string(" + r + ")" } + return r } } + + return base() +} + +// see the ascii table https://www.asciitable.com/ +func startsWithCapitalLetter(v string) bool { + return v[0] > 64 && v[0] < 91 +} + +// see the ascii table https://www.asciitable.com/ +func startsWithNumber(v string) bool { + return v[0] > 47 && v[0] < 58 } func (v field) argType(withPointer ...bool) string { - baseType := func() string { - switch v.format { - case "date-time", "date": - return "time.Time" - case "int64", "int32": - return v.format - case "double", "number": - return "float64" - case "float": - return "float32" - default: - switch v.v { - case "integer": - return "int" - case "boolean": - return "bool" - case "number": - return "float64" - } - return v.v - } + switch { + // DECISION: do not use pointers for 'optional' slices + // rationale: slice is a pointer to an array already + case v.isArray: + return "[]" + v.argItemType() + case len(withPointer) > 0 && withPointer[0]: + return "*" + v.argItemType() + default: + return v.argItemType() } +} - if len(withPointer) > 0 && withPointer[0] { - return "*" + baseType() +func (v field) argItemType() string { + switch v.format { + case "date-time", "date": + return "time.Time" + case "int64", "int32": + return v.format + case "double", "number": + return "float64" + case "float": + return "float32" + default: + switch v.v { + case "integer": + return "int" + case "boolean": + return "bool" + case "number": + return "float64" + } + return v.v } - - return baseType() } func (v field) generateDummy() interface{} { - if v.v[:2] == "[]" { - return []interface{}{field{v: v.v[2:], format: v.format}.generateDummy()} + switch { + case v.isArray: + el := field{v: v.v, format: v.format} + return "[]" + el.argItemType() + "{" + fmt.Sprintf("%v", el.generateDummyElement()) + "}" + default: + return v.generateDummyElement() } +} +func (v field) generateDummyElement() interface{} { switch v.format { case "date-time", "date": return "time.Time{}" @@ -1173,6 +1237,7 @@ type model struct { primitive fieldType name, description string generated bool + isEnum bool } func (m *model) setPrimitiveType(t fieldType) { @@ -1184,6 +1249,10 @@ func (m *model) setDescription(s string) { } func (m model) generateCode() string { + if m.isEnum { + return m.generateCodeEnum() + } + k := m.name if m.primitive.name != "" { return m.docString() + "type " + k + " " + m.primitive.argType() @@ -1247,6 +1316,32 @@ func (m model) orderedFieldNames() []string { return o } +func (m model) generateCodeEnum() string { + tmp := m.docString() + "type " + m.name + " string\n\n" + tmp += "const (\n" + for child, _ := range m.children { + enumOption := strings.ToUpper(child[:1]) + child[1:] + + enumOption = removeSpecialCharAndMakeCamelCase(enumOption, "-") + enumOption = removeSpecialCharAndMakeCamelCase(enumOption, "_") + + tmp += m.name + enumOption + " " + m.name + " = \"" + child + "\"\n" + } + tmp += ")" + return tmp +} + +func removeSpecialCharAndMakeCamelCase(s string, specialChar string) string { + els := strings.Split(s, specialChar) + s = els[0] + if len(els) > 1 { + for _, el := range els[1:] { + s += strings.ToUpper(el[:1]) + el[1:] + } + } + return s +} + func docString(name string, description string) string { o := "" for i, s := range strings.Split(strings.TrimRight(description, "\n"), "\n") { @@ -1290,9 +1385,8 @@ func generateEndpointsImplementationMethods( operations := p.Operations() - for _, httpMethod := range httpMethods { - ops, ok := operations[httpMethod] - if !ok { + for httpMethod, ops := range operations { + if !slices.Contains(httpMethods, httpMethod) { continue } @@ -1302,7 +1396,6 @@ func generateEndpointsImplementationMethods( Route: route, Description: ops.Description, } - // read common parameters for all methods pp := p.Parameters pp = append(pp, ops.Parameters...) @@ -1359,13 +1452,39 @@ func extractParameters(params openapi3.Parameters) []field { for i, p := range params { o[i] = field{ k: p.Value.Name, - v: p.Value.Schema.Value.Type, description: p.Value.Description, - format: p.Value.Schema.Value.Format, required: p.Value.Required, isInPath: p.Value.In == openapi3.ParameterInPath, isInQuery: p.Value.In == openapi3.ParameterInQuery, } + + if p.Value.Schema.Value != nil { + tmp := extractItemFromRef(p.Value.Schema) + o[i].v = tmp.v + o[i].format = tmp.format + + if p.Value.Schema.Value.Type == "array" { + item := extractItemFromRef(p.Value.Schema.Value.Items) + o[i].v = item.v + o[i].format = item.format + o[i].isArray = true + } + + } else { + o[i].v = modelNameFromRef(p.Value.Schema.Ref) + } + } + return o +} + +func extractItemFromRef(v *openapi3.SchemaRef) field { + var o field + switch { + case v.Value != nil: + o.v = v.Value.Type + o.format = v.Value.Format + default: + o.v = modelNameFromRef(v.Ref) } return o } @@ -1501,6 +1620,17 @@ func addFromValue(m models, k string, v *openapi3.Schema) { }, ) tmp.setDescription(v.Description) + + if len(v.Enum) > 0 { + tmp.isEnum = true + + tmp.children = make(map[string]struct{}, len(v.Enum)) + for _, el := range v.Enum { + child := fmt.Sprintf("%v", el) + tmp.children[child] = struct{}{} + } + } + m[k] = tmp } } diff --git a/generator/generator_test.go b/generator/generator_test.go index cd06600..f741056 100644 --- a/generator/generator_test.go +++ b/generator/generator_test.go @@ -214,9 +214,11 @@ func (c Client) ListProjects(cursor *string, limit *int, orgID *string) (ListPro Description: `Retrieves information about the specified project. foo bar qux`, - RequestBodyStruct: nil, - ResponseStruct: &model{name: "ProjectsResponse"}, - RequestParametersPath: []field{{"project_id", "string", "", "", true, true, false}}, + RequestBodyStruct: nil, + ResponseStruct: &model{name: "ProjectsResponse"}, + RequestParametersPath: []field{{"project_id", "string", + "", "", + false, true, true, false}}, }, want: `// GetProject Retrieves information about the specified project. // foo bar @@ -239,8 +241,8 @@ func (c Client) GetProject(projectID string) (ProjectsResponse, error) { RequestBodyStruct: nil, ResponseStruct: &model{name: "DatabasesResponse"}, RequestParametersPath: []field{ - {"project_id", "string", "", "", true, true, false}, - {"branch_id", "string", "", "", true, true, false}, + {"project_id", "string", "", "", false, true, true, false}, + {"branch_id", "string", "", "", false, true, true, false}, }, }, want: `// ListProjectBranchDatabases Retrieves a list of databases for the specified branch @@ -261,7 +263,7 @@ func (c Client) ListProjectBranchDatabases(projectID string, branchID string) (D Description: "Revokes the specified API key", RequestBodyStruct: nil, ResponseStruct: &model{name: "ApiKeyRevokeResponse"}, - RequestParametersPath: []field{{"key_id", "integer", "int64", "", true, true, false}}, + RequestParametersPath: []field{{"key_id", "integer", "int64", "", false, true, true, false}}, }, want: `// RevokeApiKey Revokes the specified API key func (c Client) RevokeApiKey(keyID int64) (ApiKeyRevokeResponse, error) { @@ -290,6 +292,109 @@ func (c Client) CreateProject(cfg *ProjectCreateRequest) (CreatedProject, error) return CreatedProject{}, err } return v, nil +}`, + }, + { + name: "shall generate a method to accept string enums", + fields: fields{ + Name: "GetConsumptionHistoryPerAccount", + Method: "GET", + Route: "/consumption_history/account", + Description: "Retrieves consumption metrics for Scale plan accounts. History begins at the time of upgrade.\nAvailable for Scale plan users only.\n", + ResponseStruct: &model{name: "ConsumptionHistoryPerAccountResponse"}, + RequestParametersQuery: []field{ + { + k: "from", + v: "string", + format: "date-time", + required: true, + isInQuery: true, + }, + { + k: "to", + v: "string", + format: "date-time", + required: true, + isInQuery: true, + }, + { + k: "granularity", + v: "ConsumptionHistoryGranularity", + description: "Specify the granularity of consumption metrics.\nHourly, daily, and monthly metrics are available for the last 168 hours, 60 days,\nand 1 year, respectively.\n", + required: true, + isInQuery: true, + }, + }, + }, + want: `// GetConsumptionHistoryPerAccount Retrieves consumption metrics for Scale plan accounts. History begins at the time of upgrade. +// Available for Scale plan users only. +func (c Client) GetConsumptionHistoryPerAccount(from time.Time, to time.Time, granularity ConsumptionHistoryGranularity) (ConsumptionHistoryPerAccountResponse, error) { + var ( + queryElements []string + query string + ) + queryElements = append(queryElements, "from="+from.Format(time.RFC3339)) + queryElements = append(queryElements, "to="+to.Format(time.RFC3339)) + queryElements = append(queryElements, "granularity="+string(granularity)) + if len(queryElements) > 0 { + query = "?" + strings.Join(queryElements, "&") + } + var v ConsumptionHistoryPerAccountResponse + if err := c.requestHandler(c.baseURL+"/consumption_history/account" + query, "GET", nil, &v); err != nil { + return ConsumptionHistoryPerAccountResponse{}, err + } + return v, nil +}`, + }, + { + name: "shall generate a method to accept array query attributes: []string and []int64", + fields: fields{ + Name: "GetConsumptionHistoryPerProject", + Method: "GET", + Route: "/consumption_history/projects", + Description: "Retrieves consumption metrics for Scale plan projects. History begins at the time of upgrade.\nAvailable for Scale plan users only.\n", + ResponseStruct: &model{name: "ConsumptionHistoryPerProjectResponse"}, + RequestParametersQuery: []field{ + { + k: "project_ids", + v: "string", + isArray: true, + isInQuery: true, + }, + { + k: "v", + v: "integer", + format: "int64", + isArray: true, + isInQuery: true, + }, + }, + }, + want: `// GetConsumptionHistoryPerProject Retrieves consumption metrics for Scale plan projects. History begins at the time of upgrade. +// Available for Scale plan users only. +func (c Client) GetConsumptionHistoryPerProject(projectIDs []string, v []int64) (ConsumptionHistoryPerProjectResponse, error) { + var ( + queryElements []string + query string + ) + if len(projectIDs) > 0 { + queryElements = append(queryElements, "project_ids="+strings.Join(projectIDs, ",")) + } + if len(v) > 0 { + var vTmp = make([]string, len(v)) + for i, el := range v { + vTmp[i] = fmt.Sprintf("%v", el) + } + queryElements = append(queryElements, "v="+strings.Join(vTmp, ",")) + } + if len(queryElements) > 0 { + query = "?" + strings.Join(queryElements, "&") + } + var v ConsumptionHistoryPerProjectResponse + if err := c.requestHandler(c.baseURL+"/consumption_history/projects" + query, "GET", nil, &v); err != nil { + return ConsumptionHistoryPerProjectResponse{}, err + } + return v, nil }`, }, } @@ -857,6 +962,14 @@ func Test_parameterPath_routeElement(t *testing.T) { }, want: `func (quxxID bool) string { if quxxID { return "true" }; return "false" } (quxxID)`, }, + { + name: "reference to a string enum model", + fields: fields{ + k: "foo_id", + v: "Foo", + }, + want: "string(fooID)", + }, } for _, tt := range tests { t.Run( @@ -1080,6 +1193,38 @@ EndpointResponse }, }, }, + { + name: "shall extract enum", + args: args{ + spec: openAPISpec{ + T: openapi3.T{ + OpenAPI: "3.0.3", + Components: openapi3.Components{ + Schemas: openapi3.Schemas{ + "ConsumptionHistoryGranularity": &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: "string", + Enum: []interface{}{ + "hourly", + }, + }, + }, + }, + }, + }, + }, + }, + want: map[string]model{ + "ConsumptionHistoryGranularity": { + children: map[string]struct{}{ + "hourly": {}, + }, + primitive: fieldType{name: "string"}, + name: "ConsumptionHistoryGranularity", + isEnum: true, + }, + }, + }, } for _, tt := range tests { t.Run( @@ -1166,6 +1311,72 @@ FooResponse type Foo string`, }, }, + { + name: "shall generate an enum", + v: models{ + "ConsumptionHistoryGranularity": { + children: map[string]struct{}{ + "hourly": {}, + }, + primitive: fieldType{ + name: "string", + }, + name: "ConsumptionHistoryGranularity", + isEnum: true, + }, + }, + want: []string{ + `type ConsumptionHistoryGranularity string + +const ( +ConsumptionHistoryGranularityHourly ConsumptionHistoryGranularity = "hourly" +)`, + }, + }, + { + name: "shall generate an enum with dash", + v: models{ + "Foo": { + children: map[string]struct{}{ + "foo-bar": {}, + }, + primitive: fieldType{ + name: "string", + }, + name: "Foo", + isEnum: true, + }, + }, + want: []string{ + `type Foo string + +const ( +FooFooBar Foo = "foo-bar" +)`, + }, + }, + { + name: "shall generate an enum with underscore", + v: models{ + "Foo": { + children: map[string]struct{}{ + "aws_v2": {}, + }, + primitive: fieldType{ + name: "string", + }, + name: "Foo", + isEnum: true, + }, + }, + want: []string{ + `type Foo string + +const ( +FooAwsV2 Foo = "aws_v2" +)`, + }, + }, } for _, tt := range tests { t.Run( @@ -1211,7 +1422,7 @@ func Test_objNameGoConventionExport(t *testing.T) { { name: "connection_uris", args: args{"connection_uris"}, - want: "ConnectionUris", + want: "ConnectionURIs", }, { name: "to", @@ -1602,3 +1813,217 @@ func Test_endpointImplementation_generateMethodImplementationTest(t *testing.T) ) } } + +func Test_extractParameters(t *testing.T) { + type args struct { + params openapi3.Parameters + } + tests := []struct { + name string + args args + want []field + }{ + { + name: "shall extract two required query parameters, one of which refers to schemas", + args: args{ + params: openapi3.Parameters{ + { + Value: &openapi3.Parameter{ + Name: "from", + In: openapi3.ParameterInQuery, + Description: "Specify the start `date-time` for the consumption period.\nThe `date-time` value is rounded according to the specified `granularity`.\nFor example, `2024-03-15T15:30:00Z` for `daily` granularity will be rounded to `2024-03-15T00:00:00Z`.\nThe specified `date-time` value must respect the specified granularity:\n- For `hourly`, consumption metrics are limited to the last 168 hours.\n- For `daily`, consumption metrics are limited to the last 60 days.\n- For `monthly`, consumption metrics are limited to the past year.\n\nThe consumption history is available starting from `March 1, 2024, at 00:00:00 UTC`.\n", + Required: true, + Schema: &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: "string", + Format: "date-time", + }, + }, + }, + }, + { + Value: &openapi3.Parameter{ + Name: "granularity", + In: openapi3.ParameterInQuery, + Description: "Specify the granularity of consumption metrics.\nHourly, daily, and monthly metrics are available for the last 168 hours, 60 days,\nand 1 year, respectively.\n", + Required: true, + Schema: &openapi3.SchemaRef{ + Ref: "#/components/schemas/ConsumptionHistoryGranularity", + }, + }, + }, + }, + }, + want: []field{ + { + k: "from", + v: "string", + format: "date-time", + description: "Specify the start `date-time` for the consumption period.\nThe `date-time` value is rounded according to the specified `granularity`.\nFor example, `2024-03-15T15:30:00Z` for `daily` granularity will be rounded to `2024-03-15T00:00:00Z`.\nThe specified `date-time` value must respect the specified granularity:\n- For `hourly`, consumption metrics are limited to the last 168 hours.\n- For `daily`, consumption metrics are limited to the last 60 days.\n- For `monthly`, consumption metrics are limited to the past year.\n\nThe consumption history is available starting from `March 1, 2024, at 00:00:00 UTC`.\n", + required: true, + isInQuery: true, + }, + { + k: "granularity", + v: "ConsumptionHistoryGranularity", + description: "Specify the granularity of consumption metrics.\nHourly, daily, and monthly metrics are available for the last 168 hours, 60 days,\nand 1 year, respectively.\n", + required: true, + isInQuery: true, + }, + }, + }, + { + name: "shall parameter of the type array of strings", + args: args{ + params: openapi3.Parameters{ + { + Value: &openapi3.Parameter{ + Name: "project_ids", + In: openapi3.ParameterInQuery, + Required: true, + Schema: &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: "array", + Items: &openapi3.SchemaRef{ + Value: &openapi3.Schema{Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + want: []field{ + { + k: "project_ids", + v: "string", + isArray: true, + required: true, + isInQuery: true, + }, + }, + }, + { + name: "shall parameter of the type array of int64", + args: args{ + params: openapi3.Parameters{ + { + Value: &openapi3.Parameter{ + Name: "foo", + In: openapi3.ParameterInQuery, + Required: true, + Schema: &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: "array", + Items: &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: "integer", + Format: "int64", + }, + }, + }, + }, + }, + }, + }, + }, + want: []field{ + { + k: "foo", + v: "integer", + format: "int64", + isArray: true, + required: true, + isInQuery: true, + }, + }, + }, + { + name: "shall parameter of the type array of custom type", + args: args{ + params: openapi3.Parameters{ + { + Value: &openapi3.Parameter{ + Name: "foo", + In: openapi3.ParameterInQuery, + Required: true, + Schema: &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: "array", + Items: &openapi3.SchemaRef{ + Ref: "#/components/schema/Bar", + }, + }, + }, + }, + }, + }, + }, + want: []field{ + { + k: "foo", + v: "Bar", + isArray: true, + required: true, + isInQuery: true, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, extractParameters(tt.args.params), "extractParameters(%v)", tt.args.params) + }) + } +} + +func Test_field_generateDummy(t *testing.T) { + type fields struct { + k string + v string + format string + description string + isArray bool + required bool + isInPath bool + isInQuery bool + } + tests := []struct { + name string + fields fields + want interface{} + }{ + { + name: "shall generate a dummy array of strings", + fields: fields{ + v: "string", + isArray: true, + }, + want: "[]string{\"foo\"}", + }, + { + name: "shall generate a dummy array of int64", + fields: fields{ + v: "integer", + format: "int64", + isArray: true, + }, + want: "[]int64{1}", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := field{ + k: tt.fields.k, + v: tt.fields.v, + format: tt.fields.format, + description: tt.fields.description, + isArray: tt.fields.isArray, + required: tt.fields.required, + isInPath: tt.fields.isInPath, + isInQuery: tt.fields.isInQuery, + } + assert.Equalf(t, tt.want, v.generateDummy(), "generateDummy()") + }) + } +} diff --git a/generator/templates/mockhttp.go.templ b/generator/templates/mockhttp.go.templ index 1618a62..21b7491 100644 --- a/generator/templates/mockhttp.go.templ +++ b/generator/templates/mockhttp.go.templ @@ -93,12 +93,15 @@ type objPath struct { } func parsePath(s string) objPath { - // pass through the user's endpoints - if strings.HasPrefix(s, "/users/me") { - return objPath{ - path: s, - } - } + switch s { + // pass through + case "/user/me", + "/consumption_history/account", + "/consumption_history/projects": + return objPath{ + path: s, + } + } s = strings.TrimPrefix(s, "/") o := "" @@ -159,4 +162,4 @@ func authErrorResp(req *http.Request) *http.Response { return o.httpResp() } return nil -} +} \ No newline at end of file diff --git a/generator/templates/mockhttp_test.go.templ b/generator/templates/mockhttp_test.go.templ index 7c332de..76abff8 100644 --- a/generator/templates/mockhttp_test.go.templ +++ b/generator/templates/mockhttp_test.go.templ @@ -115,4 +115,4 @@ func Test_authErrorResp(t *testing.T) { }, ) } -} +} \ No newline at end of file diff --git a/generator/templates/sdk.go.templ b/generator/templates/sdk.go.templ index 9bf0f1c..123f324 100644 --- a/generator/templates/sdk.go.templ +++ b/generator/templates/sdk.go.templ @@ -110,4 +110,4 @@ func (c Client) requestHandler(url string, t string, reqPayload interface{}, res {{ range .Types }} {{.}} -{{ end }} +{{ end }} \ No newline at end of file diff --git a/generator/templates/sdk_test.go.templ b/generator/templates/sdk_test.go.templ index 23354b4..1e63e9c 100644 --- a/generator/templates/sdk_test.go.templ +++ b/generator/templates/sdk_test.go.templ @@ -479,4 +479,4 @@ type dummyType interface { func createPointer[V dummyType](v V) *V { return &v -} +} \ No newline at end of file diff --git a/mockhttp.go b/mockhttp.go index d520db5..cff14ae 100644 --- a/mockhttp.go +++ b/mockhttp.go @@ -34,6 +34,20 @@ var endpointResponseExamples = map[string]map[string]mockResponse{ }, }, + "/consumption_history/account": { + "GET": mockResponse{ + Content: `null`, + Code: 200, + }, + }, + + "/consumption_history/projects": { + "GET": mockResponse{ + Content: `null`, + Code: 200, + }, + }, + "/projects": { "GET": mockResponse{ Content: `{"projects":[{"active_time":100,"branch_logical_size_limit":0,"branch_logical_size_limit_bytes":10800,"cpu_used_sec":0,"created_at":"2022-11-23T17:42:25Z","creation_source":"console","id":"shiny-wind-028834","name":"shiny-wind-028834","owner_id":"1232111","pg_version":15,"platform_id":"aws","provisioner":"k8s-pod","proxy_host":"us-east-2.aws.neon.tech","region_id":"aws-us-east-2","store_passwords":true,"updated_at":"2022-11-23T17:42:25Z"},{"active_time":100,"branch_logical_size_limit":0,"branch_logical_size_limit_bytes":10800,"cpu_used_sec":0,"created_at":"2022-11-23T17:52:25Z","creation_source":"console","id":"winter-boat-259881","name":"winter-boat-259881","org_id":"org-morning-bread-81040908","owner_id":"1232111","pg_version":15,"platform_id":"aws","provisioner":"k8s-pod","proxy_host":"us-east-2.aws.neon.tech","region_id":"aws-us-east-2","store_passwords":true,"updated_at":"2022-11-23T17:52:25Z"}]}`, @@ -166,7 +180,7 @@ var endpointResponseExamples = map[string]map[string]mockResponse{ Code: 200, }, "GET": mockResponse{ - Content: `{"project":{"active_time_seconds":100,"branch_logical_size_limit":0,"branch_logical_size_limit_bytes":10500,"compute_time_seconds":100,"consumption_period_end":"2023-03-01T00:00:00Z","consumption_period_start":"2023-02-01T00:00:00Z","cpu_used_sec":10,"created_at":"2022-11-23T17:42:25Z","creation_source":"console","data_storage_bytes_hour":1040,"data_transfer_bytes":1000000,"history_retention_seconds":604800,"id":"shiny-wind-028834","name":"shiny-wind-028834","owner":{"branches_limit":10,"email":"some@email.com","subscription_type":"pro"},"owner_id":"1232111","pg_version":15,"platform_id":"aws","provisioner":"k8s-pod","proxy_host":"us-east-2.aws.neon.tech","region_id":"aws-us-east-2","store_passwords":true,"updated_at":"2022-11-23T17:42:25Z","written_data_bytes":100800}}`, + Content: `{"project":{"active_time_seconds":100,"branch_logical_size_limit":0,"branch_logical_size_limit_bytes":10500,"compute_time_seconds":100,"consumption_period_end":"2023-03-01T00:00:00Z","consumption_period_start":"2023-02-01T00:00:00Z","cpu_used_sec":10,"created_at":"2022-11-23T17:42:25Z","creation_source":"console","data_storage_bytes_hour":1040,"data_transfer_bytes":1000000,"history_retention_seconds":604800,"id":"shiny-wind-028834","name":"shiny-wind-028834","owner":{"branches_limit":10,"email":"some@email.com","subscription_type":"scale"},"owner_id":"1232111","pg_version":15,"platform_id":"aws","provisioner":"k8s-pod","proxy_host":"us-east-2.aws.neon.tech","region_id":"aws-us-east-2","store_passwords":true,"updated_at":"2022-11-23T17:42:25Z","written_data_bytes":100800}}`, Code: 200, }, "PATCH": mockResponse{ @@ -177,7 +191,7 @@ var endpointResponseExamples = map[string]map[string]mockResponse{ "/projects/{project_id}/branches": { "GET": mockResponse{ - Content: `{"branches":[{"active_time_seconds":100,"compute_time_seconds":100,"cpu_used_sec":100,"created_at":"2022-11-23T17:42:25Z","creation_source":"console","current_state":"ready","data_transfer_bytes":1000000,"id":"br-aged-salad-637688","logical_size":28,"name":"main","primary":true,"project_id":"shiny-wind-028834","protected":false,"updated_at":"2022-11-23T17:42:26Z","written_data_bytes":100800},{"active_time_seconds":100,"compute_time_seconds":100,"cpu_used_sec":100,"created_at":"2022-11-30T19:09:48Z","creation_source":"console","current_state":"ready","data_transfer_bytes":1000000,"id":"br-sweet-breeze-497520","logical_size":28,"name":"dev2","parent_id":"br-aged-salad-637688","parent_lsn":"0/1DE2850","primary":true,"project_id":"shiny-wind-028834","protected":false,"updated_at":"2022-11-30T19:09:49Z","written_data_bytes":100800},{"active_time_seconds":100,"compute_time_seconds":100,"cpu_used_sec":100,"created_at":"2022-11-30T17:36:57Z","creation_source":"console","current_state":"ready","data_transfer_bytes":1000000,"id":"br-raspy-hill-832856","logical_size":21,"name":"dev1","parent_id":"br-aged-salad-637688","parent_lsn":"0/19623D8","primary":true,"project_id":"shiny-wind-028834","protected":false,"updated_at":"2022-11-30T17:36:57Z","written_data_bytes":100800}]}`, + Content: `{"branches":[{"active_time_seconds":100,"compute_time_seconds":100,"cpu_used_sec":100,"created_at":"2022-11-23T17:42:25Z","creation_source":"console","current_state":"ready","data_transfer_bytes":1000000,"default":true,"id":"br-aged-salad-637688","logical_size":28,"name":"main","primary":true,"project_id":"shiny-wind-028834","protected":false,"updated_at":"2022-11-23T17:42:26Z","written_data_bytes":100800},{"active_time_seconds":100,"compute_time_seconds":100,"cpu_used_sec":100,"created_at":"2022-11-30T19:09:48Z","creation_source":"console","current_state":"ready","data_transfer_bytes":1000000,"default":true,"id":"br-sweet-breeze-497520","logical_size":28,"name":"dev2","parent_id":"br-aged-salad-637688","parent_lsn":"0/1DE2850","primary":true,"project_id":"shiny-wind-028834","protected":false,"updated_at":"2022-11-30T19:09:49Z","written_data_bytes":100800},{"active_time_seconds":100,"compute_time_seconds":100,"cpu_used_sec":100,"created_at":"2022-11-30T17:36:57Z","creation_source":"console","current_state":"ready","data_transfer_bytes":1000000,"default":true,"id":"br-raspy-hill-832856","logical_size":21,"name":"dev1","parent_id":"br-aged-salad-637688","parent_lsn":"0/19623D8","primary":true,"project_id":"shiny-wind-028834","protected":false,"updated_at":"2022-11-30T17:36:57Z","written_data_bytes":100800}]}`, Code: 200, }, "POST": mockResponse{ @@ -252,15 +266,15 @@ var endpointResponseExamples = map[string]map[string]mockResponse{ "/projects/{project_id}/branches/{branch_id}": { "DELETE": mockResponse{ - Content: `{"branch":{"active_time_seconds":100,"compute_time_seconds":100,"cpu_used_sec":100,"created_at":"2022-11-23T17:42:25Z","creation_source":"console","current_state":"ready","data_transfer_bytes":1000000,"id":"br-aged-salad-637688","logical_size":28,"name":"main","primary":true,"project_id":"shiny-wind-028834","protected":false,"updated_at":"2022-11-23T17:42:26Z","written_data_bytes":100800},"operations":[{"action":"suspend_compute","branch_id":"br-sweet-breeze-497520","created_at":"2022-12-01T19:53:05Z","endpoint_id":"ep-soft-violet-752733","failures_count":0,"id":"b6afbc21-2990-4a76-980b-b57d8c2948f2","project_id":"shiny-wind-028834","status":"running","total_duration_ms":100,"updated_at":"2022-12-01T19:53:05Z"},{"action":"delete_timeline","branch_id":"br-sweet-breeze-497520","created_at":"2022-12-01T19:53:05Z","failures_count":0,"id":"b6afbc21-2990-4a76-980b-b57d8c2948f2","project_id":"shiny-wind-028834","status":"scheduling","total_duration_ms":100,"updated_at":"2022-12-01T19:53:05Z"}]}`, + Content: `{"branch":{"active_time_seconds":100,"compute_time_seconds":100,"cpu_used_sec":100,"created_at":"2022-11-23T17:42:25Z","creation_source":"console","current_state":"ready","data_transfer_bytes":1000000,"default":true,"id":"br-aged-salad-637688","logical_size":28,"name":"main","primary":true,"project_id":"shiny-wind-028834","protected":false,"updated_at":"2022-11-23T17:42:26Z","written_data_bytes":100800},"operations":[{"action":"suspend_compute","branch_id":"br-sweet-breeze-497520","created_at":"2022-12-01T19:53:05Z","endpoint_id":"ep-soft-violet-752733","failures_count":0,"id":"b6afbc21-2990-4a76-980b-b57d8c2948f2","project_id":"shiny-wind-028834","status":"running","total_duration_ms":100,"updated_at":"2022-12-01T19:53:05Z"},{"action":"delete_timeline","branch_id":"br-sweet-breeze-497520","created_at":"2022-12-01T19:53:05Z","failures_count":0,"id":"b6afbc21-2990-4a76-980b-b57d8c2948f2","project_id":"shiny-wind-028834","status":"scheduling","total_duration_ms":100,"updated_at":"2022-12-01T19:53:05Z"}]}`, Code: 200, }, "GET": mockResponse{ - Content: `{"branch":{"active_time_seconds":100,"compute_time_seconds":100,"cpu_used_sec":100,"created_at":"2022-11-23T17:42:25Z","creation_source":"console","current_state":"ready","data_transfer_bytes":1000000,"id":"br-aged-salad-637688","logical_size":28,"name":"main","primary":true,"project_id":"shiny-wind-028834","protected":false,"updated_at":"2022-11-23T17:42:26Z","written_data_bytes":100800}}`, + Content: `{"branch":{"active_time_seconds":100,"compute_time_seconds":100,"cpu_used_sec":100,"created_at":"2022-11-23T17:42:25Z","creation_source":"console","current_state":"ready","data_transfer_bytes":1000000,"default":true,"id":"br-aged-salad-637688","logical_size":28,"name":"main","primary":true,"project_id":"shiny-wind-028834","protected":false,"updated_at":"2022-11-23T17:42:26Z","written_data_bytes":100800}}`, Code: 200, }, "PATCH": mockResponse{ - Content: `{"branch":{"active_time_seconds":100,"compute_time_seconds":100,"cpu_used_sec":100,"created_at":"2022-11-23T17:42:25Z","creation_source":"console","current_state":"ready","data_transfer_bytes":1000000,"id":"br-icy-dream-250089","name":"mybranch","parent_id":"br-aged-salad-637688","parent_lsn":"0/1E19478","primary":true,"project_id":"shiny-wind-028834","protected":false,"updated_at":"2022-11-23T17:42:26Z","written_data_bytes":100800},"operations":[]}`, + Content: `{"branch":{"active_time_seconds":100,"compute_time_seconds":100,"cpu_used_sec":100,"created_at":"2022-11-23T17:42:25Z","creation_source":"console","current_state":"ready","data_transfer_bytes":1000000,"default":true,"id":"br-icy-dream-250089","name":"mybranch","parent_id":"br-aged-salad-637688","parent_lsn":"0/1E19478","primary":true,"project_id":"shiny-wind-028834","protected":false,"updated_at":"2022-11-23T17:42:26Z","written_data_bytes":100800},"operations":[]}`, Code: 200, }, }, @@ -359,9 +373,23 @@ var endpointResponseExamples = map[string]map[string]mockResponse{ }, }, + "/projects/{project_id}/branches/{branch_id}/schema": { + "GET": mockResponse{ + Content: `null`, + Code: 200, + }, + }, + + "/projects/{project_id}/branches/{branch_id}/set_as_default": { + "POST": mockResponse{ + Content: `{"branch":{"active_time_seconds":1,"compute_time_seconds":1,"cpu_used_sec":1,"created_at":"2022-11-23T17:42:25Z","creation_source":"console","current_state":"ready","data_transfer_bytes":100,"default":true,"id":"br-icy-dream-250089","name":"mybranch","parent_id":"br-aged-salad-637688","parent_lsn":"0/1E19478","primary":true,"project_id":"shiny-wind-028834","protected":false,"updated_at":"2022-11-23T17:42:26Z","written_data_bytes":100},"operations":[]}`, + Code: 200, + }, + }, + "/projects/{project_id}/branches/{branch_id}/set_as_primary": { "POST": mockResponse{ - Content: `{"branch":{"active_time_seconds":1,"compute_time_seconds":1,"cpu_used_sec":1,"created_at":"2022-11-23T17:42:25Z","creation_source":"console","current_state":"ready","data_transfer_bytes":100,"id":"br-icy-dream-250089","name":"mybranch","parent_id":"br-aged-salad-637688","parent_lsn":"0/1E19478","primary":true,"project_id":"shiny-wind-028834","protected":false,"updated_at":"2022-11-23T17:42:26Z","written_data_bytes":100},"operations":[]}`, + Content: `{"branch":{"active_time_seconds":1,"compute_time_seconds":1,"cpu_used_sec":1,"created_at":"2022-11-23T17:42:25Z","creation_source":"console","current_state":"ready","data_transfer_bytes":100,"default":true,"id":"br-icy-dream-250089","name":"mybranch","parent_id":"br-aged-salad-637688","parent_lsn":"0/1E19478","primary":true,"project_id":"shiny-wind-028834","protected":false,"updated_at":"2022-11-23T17:42:26Z","written_data_bytes":100},"operations":[]}`, Code: 200, }, }, @@ -502,14 +530,21 @@ var endpointResponseExamples = map[string]map[string]mockResponse{ Code: 200, }, }, + + "/users/me/organizations": { + "GET": mockResponse{ + Content: `null`, + Code: 200, + }, + }, } // NewMockHTTPClient initiates a mock fo the HTTP client required for the SDK client. // Mock client return the response as per API spec, except for the errors: 404 and 401 status codes are covered only. // - 401 is returned when the string `invalidApiKey` is used as the API key; // - 404 is returned if either of the following: -// - the string value `notFound` is used as the string argument, e.g. projectID -// - a negative int/float value is used as the int/float argument, e.g. database ID +// - the string value `notFound` is used as the string argument, e.g. projectID +// - a negative int/float value is used as the int/float argument, e.g. database ID func NewMockHTTPClient() HTTPClient { u, _ := url.Parse(baseURL) return mockHTTPClient{ @@ -578,8 +613,11 @@ type objPath struct { } func parsePath(s string) objPath { - // pass through the user's endpoints - if strings.HasPrefix(s, "/users/me") { + switch s { + // pass through + case "/user/me", + "/consumption_history/account", + "/consumption_history/projects": return objPath{ path: s, } diff --git a/openAPIDefinition.json b/openAPIDefinition.json index cab8c56..7fa94d1 100644 --- a/openAPIDefinition.json +++ b/openAPIDefinition.json @@ -287,7 +287,7 @@ }, { "name": "org_id", - "description": "Search for projects by `org_id` (Comming soon).", + "description": "Search for projects by `org_id`.", "in": "query", "schema": { "type": "string" @@ -579,7 +579,7 @@ "owner": { "email": "some@email.com", "branches_limit": 10, - "subscription_type": "pro" + "subscription_type": "scale" }, "creation_source": "console", "store_passwords": true, @@ -904,7 +904,7 @@ { "name": "branch_id", "in": "query", - "description": "The branch ID. Defaults to your project's primary `branch_id` if not specified.", + "description": "The branch ID. Defaults to your project's default `branch_id` if not specified.", "required": false, "schema": { "type": "string" @@ -978,7 +978,7 @@ ], "post": { "summary": "Create a branch", - "description": "Creates a branch in the specified project.\nYou can obtain a `project_id` by listing the projects for your Neon account.\n\nThis method does not require a request body, but you can specify one to create a compute endpoint for the branch or to select a non-default parent branch.\nThe default behavior is to create a branch from the project's primary branch with no compute endpoint, and the branch name is auto-generated.\nThere is a maximum of one read-write endpoint per branch.\nA branch can have multiple read-only endpoints.\nFor related information, see [Manage branches](https://neon.tech/docs/manage/branches/).\n", + "description": "Creates a branch in the specified project.\nYou can obtain a `project_id` by listing the projects for your Neon account.\n\nThis method does not require a request body, but you can specify one to create a compute endpoint for the branch or to select a non-default parent branch.\nThe default behavior is to create a branch from the project's default branch with no compute endpoint, and the branch name is auto-generated.\nThere is a maximum of one read-write endpoint per branch.\nA branch can have multiple read-only endpoints.\nFor related information, see [Manage branches](https://neon.tech/docs/manage/branches/).\n", "tags": [ "Branch" ], @@ -1058,6 +1058,7 @@ "active_time_seconds": 100, "cpu_used_sec": 100, "primary": true, + "default": true, "protected": false, "creation_source": "console" }, @@ -1077,6 +1078,7 @@ "active_time_seconds": 100, "cpu_used_sec": 100, "primary": true, + "default": true, "protected": false, "creation_source": "console" }, @@ -1096,6 +1098,7 @@ "active_time_seconds": 100, "cpu_used_sec": 100, "primary": true, + "default": true, "protected": false, "creation_source": "console" } @@ -1133,7 +1136,7 @@ ], "get": { "summary": "Get branch details", - "description": "Retrieves information about the specified branch.\nYou can obtain a `project_id` by listing the projects for your Neon account.\nYou can obtain a `branch_id` by listing the project's branches.\nA `branch_id` value has a `br-` prefix.\n\nEach Neon project is initially created with a root and primary branch named `main`.\nA project can contain one or more branches.\nA parent branch is identified by a `parent_id` value, which is the `id` of the parent branch.\nFor related information, see [Manage branches](https://neon.tech/docs/manage/branches/).\n", + "description": "Retrieves information about the specified branch.\nYou can obtain a `project_id` by listing the projects for your Neon account.\nYou can obtain a `branch_id` by listing the project's branches.\nA `branch_id` value has a `br-` prefix.\n\nEach Neon project is initially created with a root and default branch named `main`.\nA project can contain one or more branches.\nA parent branch is identified by a `parent_id` value, which is the `id` of the parent branch.\nFor related information, see [Manage branches](https://neon.tech/docs/manage/branches/).\n", "tags": [ "Branch" ], @@ -1161,6 +1164,7 @@ "active_time_seconds": 100, "cpu_used_sec": 100, "primary": true, + "default": true, "protected": false, "creation_source": "console" } @@ -1175,7 +1179,7 @@ }, "delete": { "summary": "Delete a branch", - "description": "Deletes the specified branch from a project, and places\nall compute endpoints into an idle state, breaking existing client connections.\nYou can obtain a `project_id` by listing the projects for your Neon account.\nYou can obtain a `branch_id` by listing the project's branches.\nFor related information, see [Manage branches](https://neon.tech/docs/manage/branches/).\n\nWhen a successful response status is received, the compute endpoints are still active,\nand the branch is not yet deleted from storage.\nThe deletion occurs after all operations finish.\nYou cannot delete a project's root or primary branch, and you cannot delete a branch that has a child branch.\nA project must have at least one branch.\n", + "description": "Deletes the specified branch from a project, and places\nall compute endpoints into an idle state, breaking existing client connections.\nYou can obtain a `project_id` by listing the projects for your Neon account.\nYou can obtain a `branch_id` by listing the project's branches.\nFor related information, see [Manage branches](https://neon.tech/docs/manage/branches/).\n\nWhen a successful response status is received, the compute endpoints are still active,\nand the branch is not yet deleted from storage.\nThe deletion occurs after all operations finish.\nYou cannot delete a project's root or default branch, and you cannot delete a branch that has a child branch.\nA project must have at least one branch.\n", "tags": [ "Branch" ], @@ -1203,6 +1207,7 @@ "active_time_seconds": 100, "cpu_used_sec": 100, "primary": true, + "default": true, "protected": false, "creation_source": "console" }, @@ -1286,6 +1291,7 @@ "active_time_seconds": 100, "cpu_used_sec": 100, "primary": true, + "default": true, "protected": false, "creation_source": "console" }, @@ -1355,6 +1361,86 @@ } } }, + "/projects/{project_id}/branches/{branch_id}/schema": { + "get": { + "tags": [ + "Branch" + ], + "summary": "Get the database schema", + "description": "Retrieves the schema from the specified database. The `lsn` and `timestamp` values cannot be specified at the same time. If both are omitted, the database schema is retrieved from database's head .", + "operationId": "getProjectBranchSchema", + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The Neon project ID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "branch_id", + "in": "path", + "description": "The branch ID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "role", + "in": "query", + "description": "The role on whose behalf the schema is retrieved", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "db_name", + "in": "query", + "description": "Name of the database for which the schema is retrieved", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "lsn", + "in": "query", + "description": "The Log Sequence Number (LSN) for which the schema is retrieved\n", + "schema": { + "type": "string" + } + }, + { + "name": "timestamp", + "in": "query", + "description": "The point in time for which the schema is retrieved\n", + "schema": { + "type": "string", + "format": "date-time" + } + } + ], + "responses": { + "200": { + "description": "Schema definition", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BranchSchemaResponse" + } + } + } + }, + "default": { + "$ref": "#/components/responses/GeneralError" + } + } + } + }, "/projects/{project_id}/branches/{branch_id}/set_as_primary": { "parameters": [ { @@ -1381,7 +1467,8 @@ "Branch" ], "summary": "Set branch as primary", - "description": "Sets the specified branch as the project's primary branch.\nThe primary designation is automatically removed from the previous primary branch.\nYou can obtain a `project_id` by listing the projects for your Neon account.\nYou can obtain the `branch_id` by listing the project's branches.\nFor more information, see [Manage branches](https://neon.tech/docs/manage/branches/).\n", + "deprecated": true, + "description": "DEPRECATED. Use `/set_as_default` endpoint.\nSets the specified branch as the project's primary branch.\nThe primary designation is automatically removed from the previous primary branch.\nYou can obtain a `project_id` by listing the projects for your Neon account.\nYou can obtain the `branch_id` by listing the project's branches.\nFor more information, see [Manage branches](https://neon.tech/docs/manage/branches/).\n", "operationId": "setPrimaryProjectBranch", "responses": { "200": { @@ -1407,6 +1494,74 @@ "created_at": "2022-11-23T17:42:25Z", "updated_at": "2022-11-23T17:42:26Z", "primary": true, + "default": true, + "protected": false, + "creation_source": "console" + }, + "operations": [] + } + } + } + }, + "default": { + "$ref": "#/components/responses/GeneralError" + } + } + } + }, + "/projects/{project_id}/branches/{branch_id}/set_as_default": { + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The Neon project ID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "branch_id", + "in": "path", + "description": "The branch ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "post": { + "tags": [ + "Branch" + ], + "summary": "Set branch as default", + "description": "Sets the specified branch as the project's default branch.\nThe default designation is automatically removed from the previous default branch.\nYou can obtain a `project_id` by listing the projects for your Neon account.\nYou can obtain the `branch_id` by listing the project's branches.\nFor more information, see [Manage branches](https://neon.tech/docs/manage/branches/).\n", + "operationId": "setDefaultProjectBranch", + "responses": { + "200": { + "description": "Updated the specified branch", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BranchOperations" + }, + "example": { + "branch": { + "cpu_used_sec": 1, + "active_time_seconds": 1, + "compute_time_seconds": 1, + "written_data_bytes": 100, + "data_transfer_bytes": 100, + "id": "br-icy-dream-250089", + "project_id": "shiny-wind-028834", + "parent_id": "br-aged-salad-637688", + "parent_lsn": "0/1E19478", + "name": "mybranch", + "current_state": "ready", + "created_at": "2022-11-23T17:42:25Z", + "updated_at": "2022-11-23T17:42:26Z", + "primary": true, + "default": true, "protected": false, "creation_source": "console" }, @@ -2963,6 +3118,269 @@ } } }, + "/consumption_history/account": { + "get": { + "summary": "Get account consumption metrics", + "description": "Retrieves consumption metrics for Scale plan accounts. History begins at the time of upgrade.\nAvailable for Scale plan users only.\n", + "tags": [ + "Consumption" + ], + "operationId": "getConsumptionHistoryPerAccount", + "parameters": [ + { + "name": "from", + "description": "Specify the start `date-time` for the consumption period.\nThe `date-time` value is rounded according to the specified `granularity`.\nFor example, `2024-03-15T15:30:00Z` for `daily` granularity will be rounded to `2024-03-15T00:00:00Z`.\nThe specified `date-time` value must respect the specified granularity:\n- For `hourly`, consumption metrics are limited to the last 168 hours.\n- For `daily`, consumption metrics are limited to the last 60 days.\n- For `monthly`, consumption metrics are limited to the past year.\n\nThe consumption history is available starting from `March 1, 2024, at 00:00:00 UTC`.\n", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + }, + "required": true + }, + { + "name": "to", + "description": "Specify the end `date-time` for the consumption period.\nThe `date-time` value is rounded according to the specified granularity.\nFor example, `2024-03-15T15:30:00Z` for `daily` granularity will be rounded to `2024-03-15T00:00:00Z`.\nThe specified `date-time` value must respect the specified granularity:\n- For `hourly`, consumption metrics are limited to the last 168 hours.\n- For `daily`, consumption metrics are limited to the last 60 days.\n- For `monthly`, consumption metrics are limited to the past year.\n", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + }, + "required": true + }, + { + "name": "granularity", + "description": "Specify the granularity of consumption metrics.\nHourly, daily, and monthly metrics are available for the last 168 hours, 60 days,\nand 1 year, respectively.\n", + "in": "query", + "schema": { + "$ref": "#/components/schemas/ConsumptionHistoryGranularity" + }, + "required": true + }, + { + "name": "org_id", + "description": "Specify the organization for which the consumption metrics should be returned.\nIf this parameter is not provided, the endpoint will return the metrics for the\nauthenticated user's account.\n", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "include_v1_metrics", + "description": "Include metrics utilized in previous pricing models.\n- **data_storage_bytes_hour**: The sum of the maximum observed storage values for each hour\n for each project, which never decreases.\n", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Returned consumption metrics for the Neon account", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConsumptionHistoryPerAccountResponse" + } + } + } + }, + "403": { + "description": "This endpoint is not available. It is only supported with Scale plan accounts.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GeneralError" + } + } + } + }, + "404": { + "description": "Account is not a member of the organization specified by `org_id`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GeneralError" + } + } + } + }, + "406": { + "description": "The specified `date-time` range is outside the boundaries of the specified `granularity`.\nAdjust your `from` and `to` values or select a different `granularity`.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GeneralError" + } + } + } + }, + "429": { + "description": "Too many requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GeneralError" + } + } + } + }, + "default": { + "$ref": "#/components/responses/GeneralError" + } + } + } + }, + "/consumption_history/projects": { + "get": { + "summary": "Get consumption metrics for each project", + "description": "Retrieves consumption metrics for Scale plan projects. History begins at the time of upgrade.\nAvailable for Scale plan users only.\n", + "tags": [ + "Consumption" + ], + "operationId": "getConsumptionHistoryPerProject", + "parameters": [ + { + "name": "cursor", + "description": "Specify the cursor value from the previous response to get the next batch of projects.", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "description": "Specify a value from 1 to 100 to limit number of projects in the response.", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "default": 10, + "maximum": 100 + } + }, + { + "name": "project_ids", + "description": "Specify a list of project IDs to filter the response.\nIf omitted, the response will contain all projects.\n", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 0, + "maxItems": 100 + } + }, + { + "name": "from", + "description": "Specify the start `date-time` for the consumption period.\nThe `date-time` value is rounded according to the specified `granularity`.\nFor example, `2024-03-15T15:30:00Z` for `daily` granularity will be rounded to `2024-03-15T00:00:00Z`.\nThe specified `date-time` value must respect the specified `granularity`:\n- For `hourly`, consumption metrics are limited to the last 168 hours.\n- For `daily`, consumption metrics are limited to the last 60 days.\n- For `monthly`, consumption metrics are limited to the last year.\n\nThe consumption history is available starting from `March 1, 2024, at 00:00:00 UTC`.\n", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + }, + "required": true + }, + { + "name": "to", + "description": "Specify the end `date-time` for the consumption period.\nThe `date-time` value is rounded according to the specified granularity.\nFor example, `2024-03-15T15:30:00Z` for `daily` granularity will be rounded to `2024-03-15T00:00:00Z`.\nThe specified `date-time` value must respect the specified `granularity`:\n- For `hourly`, consumption metrics are limited to the last 168 hours.\n- For `daily`, consumption metrics are limited to the last 60 days.\n- For `monthly`, consumption metrics are limited to the last year.\n", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + }, + "required": true + }, + { + "name": "granularity", + "description": "Specify the granularity of consumption metrics.\nHourly, daily, and monthly metrics are available for the last 168 hours, 60 days,\nand 1 year, respectively.\n", + "in": "query", + "schema": { + "$ref": "#/components/schemas/ConsumptionHistoryGranularity" + }, + "required": true + }, + { + "name": "org_id", + "description": "Specify the organization for which the project consumption metrics should be returned.\nIf this parameter is not provided, the endpoint will return the metrics for the\nauthenticated user's projects.\n", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "include_v1_metrics", + "description": "Include metrics utilized in previous pricing models.\n- **data_storage_bytes_hour**: The sum of the maximum observed storage values for each hour,\n which never decreases.\n", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Returned project consumption metrics for the Neon account", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ConsumptionHistoryPerProjectResponse" + }, + { + "$ref": "#/components/schemas/PaginationResponse" + } + ] + } + } + } + }, + "403": { + "description": "This endpoint is not available. It is only supported with Scale plan accounts.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GeneralError" + } + } + } + }, + "404": { + "description": "Account is not a member of the organization specified by `org_id`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GeneralError" + } + } + } + }, + "406": { + "description": "The specified `date-time` range is outside the boundaries of the specified `granularity`.\nAdjust your `from` and `to` values or select a different `granularity`.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GeneralError" + } + } + } + }, + "429": { + "description": "Too many requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GeneralError" + } + } + } + }, + "default": { + "$ref": "#/components/responses/GeneralError" + } + } + } + }, "/consumption/projects": { "get": { "summary": "Get project consumption metrics", @@ -3008,6 +3426,14 @@ "type": "string", "format": "date-time" } + }, + { + "name": "org_id", + "description": "Specify the organization for which the project consumption metrics should be returned.\nIf this parameter is not provided, the endpoint will return the metrics for the authenticated\nuser's projects.\n", + "in": "query", + "schema": { + "type": "string" + } } ], "responses": { @@ -3028,6 +3454,16 @@ } } }, + "404": { + "description": "Account is not a member of the organization specified by `org_id`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GeneralError" + } + } + } + }, "default": { "$ref": "#/components/responses/GeneralError" } @@ -3058,6 +3494,28 @@ } } } + }, + "/users/me/organizations": { + "get": { + "summary": "Get current user organizations list", + "description": "Retrieves information about the current Neon user's organizations\n", + "operationId": "getCurrentUserOrganizations", + "responses": { + "200": { + "description": "Returned information about the current user organizations\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationsResponse" + } + } + } + }, + "default": { + "$ref": "#/components/responses/GeneralError" + } + } + } } }, "components": { @@ -3174,80 +3632,10 @@ } }, "schemas": { - "GithubAppRepository": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "description": "The ID of the repository" - }, - "name": { - "type": "string", - "description": "The name of the repository" - } - } - }, - "GithubAppProjectResponse": { - "type": "object", - "properties": { - "project_id": { - "type": "string", - "description": "The ID of the project" - }, - "installation_id": { - "type": "integer", - "format": "int64", - "description": "The ID of the GitHub App installation" - }, - "repositories": { - "type": "array", - "items": { - "$ref": "#/components/schemas/GithubAppRepository" - }, - "description": "The list of repositories associated with the GitHub App" - }, - "current_repository": { - "type": "string", - "description": "The current repository associated with the GitHub App" - }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "The timestamp when the GitHub App was created" - }, - "updated_at": { - "type": "string", - "format": "date-time", - "description": "The timestamp when the GitHub App was last updated" - }, - "current_repository_id": { - "type": "integer", - "format": "int64", - "description": "The ID of the current repository associated with the GitHub App" - }, - "role": { - "type": "string", - "description": "The neon database role" - }, - "db": { - "type": "string", - "description": "The neon database name" - } - } - }, - "RepositoryUpdateRequest": { + "Features": { "type": "object", - "required": [ - "repository_id" - ], - "properties": { - "repository_id": { - "type": "integer", - "format": "int64", - "minimum": 1, - "description": "The ID of the GitHub repository" - } + "additionalProperties": { + "type": "boolean" } }, "ComputeUnit": { @@ -3880,7 +4268,7 @@ "owner": { "email": "some@email.com", "branches_limit": 10, - "subscription_type": "pro" + "subscription_type": "scale" }, "org_id": "org-morning-bread-81040908" } @@ -4079,6 +4467,141 @@ } } }, + "ConsumptionHistoryPerAccountResponse": { + "type": "object", + "properties": { + "periods": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConsumptionHistoryPerPeriod" + } + } + }, + "required": [ + "periods" + ] + }, + "ConsumptionHistoryPerProjectResponse": { + "type": "object", + "properties": { + "projects": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConsumptionHistoryPerProject" + } + } + }, + "required": [ + "projects" + ] + }, + "ConsumptionHistoryPerProject": { + "type": "object", + "properties": { + "project_id": { + "type": "string" + }, + "periods": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConsumptionHistoryPerPeriod" + } + } + }, + "required": [ + "project_id", + "periods" + ] + }, + "ConsumptionHistoryPerPeriod": { + "type": "object", + "properties": { + "period_id": { + "type": "string", + "format": "uuid" + }, + "consumption": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConsumptionHistoryPerTimeframe" + } + } + }, + "required": [ + "period_id", + "consumption" + ], + "example": { + "period_id": "79ec829f-1828-4006-ac82-9f1828a0067d", + "consumption": [ + { + "timeframe_start": "2024-03-22T00:00:00Z", + "timeframe_end": "2024-03-23T00:00:00Z", + "active_time_seconds": 27853, + "compute_time_seconds": 18346, + "written_data_bytes": 1073741824, + "synthetic_storage_size_bytes": 5368709120 + }, + { + "timeframe_start": "2024-03-23T00:00:00Z", + "timeframe_end": "2024-03-24T00:00:00Z", + "active_time_seconds": 17498, + "compute_time_seconds": 3378, + "written_data_bytes": 5741824, + "synthetic_storage_size_bytes": 2370912 + } + ] + } + }, + "ConsumptionHistoryPerTimeframe": { + "type": "object", + "properties": { + "timeframe_start": { + "type": "string", + "format": "date-time" + }, + "timeframe_end": { + "type": "string", + "format": "date-time" + }, + "active_time_seconds": { + "type": "integer", + "format": "uint64" + }, + "compute_time_seconds": { + "type": "integer", + "format": "uint64" + }, + "written_data_bytes": { + "type": "integer", + "format": "uint64" + }, + "synthetic_storage_size_bytes": { + "type": "integer", + "format": "uint64" + }, + "data_storage_bytes_hour": { + "type": "integer", + "format": "uint64" + } + }, + "required": [ + "timeframe_start", + "timeframe_end", + "active_time_seconds", + "compute_time_seconds", + "written_data_bytes", + "synthetic_storage_size_bytes" + ] + }, + "ConsumptionHistoryGranularity": { + "type": "string", + "enum": [ + "hourly", + "daily", + "monthly" + ] + }, "ProjectsConsumptionResponse": { "type": "object", "required": [ @@ -4216,9 +4739,9 @@ }, "ProjectLimits": { "type": "object", - "additionalProperties": true, "required": [ - "limits" + "limits", + "features" ], "properties": { "limits": { @@ -4230,11 +4753,13 @@ "max_protected_branches", "max_autoscaling_cu", "cpu_seconds", + "max_compute_time_non_primary", "max_active_endpoints", "max_read_only_endpoints", "max_allowed_ips", "max_monitoring_retention_hours", - "min_autosuspend_seconds" + "min_autosuspend_seconds", + "max_data_transfer" ], "properties": { "active_time": { @@ -4258,6 +4783,10 @@ "type": "integer", "format": "int64" }, + "max_compute_time_non_primary": { + "type": "integer", + "format": "int64" + }, "max_active_endpoints": { "type": "integer" }, @@ -4272,8 +4801,15 @@ }, "min_autosuspend_seconds": { "type": "integer" + }, + "max_data_transfer": { + "type": "integer", + "format": "int64" } } + }, + "features": { + "$ref": "#/components/schemas/Features" } } }, @@ -4288,6 +4824,7 @@ "created_at", "updated_at", "primary", + "default", "protected", "cpu_used_sec", "active_time_seconds", @@ -4337,7 +4874,12 @@ "type": "string" }, "primary": { - "description": "Whether the branch is the project's primary branch\n", + "deprecated": true, + "description": "DEPRECATED. Use `default` field.\nWhether the branch is the project's primary branch\n", + "type": "boolean" + }, + "default": { + "description": "Whether the branch is the project's default branch\n", "type": "boolean" }, "protected": { @@ -4393,7 +4935,8 @@ "creation_source": "console", "created_at": "2022-11-30T19:09:48Z", "updated_at": "2022-12-01T19:53:05Z", - "primary": true + "primary": true, + "default": true } }, "BranchState": { @@ -4442,7 +4985,7 @@ "type": "object", "properties": { "parent_id": { - "description": "The `branch_id` of the parent branch. If omitted or empty, the branch will be created from the project's primary branch.\n", + "description": "The `branch_id` of the parent branch. If omitted or empty, the branch will be created from the project's default branch.\n", "type": "string" }, "name": { @@ -4789,7 +5332,7 @@ "type": "boolean" }, "primary_branch_only": { - "description": "If true, the list will be applied only to the primary branch.", + "description": "If true, the list will be applied only to the default branch.", "type": "boolean" } } @@ -5189,6 +5732,7 @@ "payment_source", "subscription_type", "quota_reset_at_last", + "name", "email", "address_city", "address_country", @@ -5209,6 +5753,10 @@ "type": "string", "format": "date-time" }, + "name": { + "description": "The full name of the individual or entity that owns the billing account. This name appears on invoices.", + "type": "string" + }, "email": { "description": "Billing email, to receive emails related to invoices and subscriptions.\n", "type": "string" @@ -5218,7 +5766,11 @@ "type": "string" }, "address_country": { - "description": "Billing address country.\n", + "description": "Billing address country code defined by ISO 3166-1 alpha-2.\n", + "type": "string" + }, + "address_country_name": { + "description": "Billing address country name.\n", "type": "string" }, "address_line1": { @@ -5240,38 +5792,14 @@ "orb_portal_url": { "description": "Orb user portal url\n", "type": "string" - } - } - }, - "BillingAccountUpdateRequest": { - "type": "object", - "required": [ - "billing_account" - ], - "properties": { - "billing_account": { - "type": "object", - "properties": { - "email": { - "description": "Billing email, to receive emails related to invoices and subscriptions.\n", - "type": "string" - }, - "org_id": { - "type": "string", - "description": "Organization id, to update the billing email of the organization account.\n" - } - } - } - } - }, - "BillingAccountResponse": { - "type": "object", - "required": [ - "billing_account" - ], - "properties": { - "billing_account": { - "$ref": "#/components/schemas/BillingAccount" + }, + "tax_id": { + "description": "The tax identification number for the billing account, displayed on invoices.\n", + "type": "string" + }, + "tax_id_type": { + "description": "The type of the tax identification number based on the country.\n", + "type": "string" } } }, @@ -5280,8 +5808,6 @@ "description": "Type of subscription to Neon Cloud.\nNotice that for users without billing account this will be \"UNKNOWN\"\n", "enum": [ "UNKNOWN", - "free", - "pro", "direct_sales", "aws_marketplace", "free_v2", @@ -5736,7 +6262,8 @@ "OrganizationLimits": { "type": "object", "required": [ - "limits" + "limits", + "features" ], "properties": { "limits": { @@ -5752,7 +6279,8 @@ "max_allowed_ips", "max_monitoring_retention_hours", "max_history_retention_seconds", - "max_compute_time_non_primary" + "max_compute_time_non_primary", + "min_autosuspend_seconds" ], "properties": { "active_time": { @@ -5792,8 +6320,14 @@ "max_compute_time_non_primary": { "type": "integer", "format": "int64" + }, + "min_autosuspend_seconds": { + "type": "integer" } } + }, + "features": { + "$ref": "#/components/schemas/Features" } } }, diff --git a/sdk.go b/sdk.go index b0a5372..f1cbe02 100644 --- a/sdk.go +++ b/sdk.go @@ -155,6 +155,22 @@ func (c Client) GetProjectOperation(projectID string, operationID string) (Opera return v, nil } +// CreateProject Creates a Neon project. +// A project is the top-level object in the Neon object hierarchy. +// Plan limits define how many projects you can create. +// Neon's Free plan permits one project per Neon account. +// For more information, see [Manage projects](https://neon.tech/docs/manage/projects/). +// You can specify a region and Postgres version in the request body. +// Neon currently supports PostgreSQL 14, 15, and 16. +// For supported regions and `region_id` values, see [Regions](https://neon.tech/docs/introduction/regions/). +func (c Client) CreateProject(cfg ProjectCreateRequest) (CreatedProject, error) { + var v CreatedProject + if err := c.requestHandler(c.baseURL+"/projects", "POST", cfg, &v); err != nil { + return CreatedProject{}, err + } + return v, nil +} + // ListProjects Retrieves a list of projects for the Neon account. // A project is the top-level object in the Neon object hierarchy. // For more information, see [Manage projects](https://neon.tech/docs/manage/projects/). @@ -185,22 +201,6 @@ func (c Client) ListProjects(cursor *string, limit *int, search *string, orgID * return v, nil } -// CreateProject Creates a Neon project. -// A project is the top-level object in the Neon object hierarchy. -// Plan limits define how many projects you can create. -// Neon's Free plan permits one project per Neon account. -// For more information, see [Manage projects](https://neon.tech/docs/manage/projects/). -// You can specify a region and Postgres version in the request body. -// Neon currently supports PostgreSQL 14, 15, and 16. -// For supported regions and `region_id` values, see [Regions](https://neon.tech/docs/introduction/regions/). -func (c Client) CreateProject(cfg ProjectCreateRequest) (CreatedProject, error) { - var v CreatedProject - if err := c.requestHandler(c.baseURL+"/projects", "POST", cfg, &v); err != nil { - return CreatedProject{}, err - } - return v, nil -} - // ListSharedProjects Retrieves a list of shared projects for the Neon account. // A project is the top-level object in the Neon object hierarchy. // For more information, see [Manage projects](https://neon.tech/docs/manage/projects/). @@ -228,6 +228,18 @@ func (c Client) ListSharedProjects(cursor *string, limit *int, search *string) ( return v, nil } +// DeleteProject Deletes the specified project. +// You can obtain a `project_id` by listing the projects for your Neon account. +// Deleting a project is a permanent action. +// Deleting a project also deletes endpoints, branches, databases, and users that belong to the project. +func (c Client) DeleteProject(projectID string) (ProjectResponse, error) { + var v ProjectResponse + if err := c.requestHandler(c.baseURL+"/projects/"+projectID, "DELETE", nil, &v); err != nil { + return ProjectResponse{}, err + } + return v, nil +} + // GetProject Retrieves information about the specified project. // A project is the top-level object in the Neon object hierarchy. // You can obtain a `project_id` by listing the projects for your Neon account. @@ -250,18 +262,6 @@ func (c Client) UpdateProject(projectID string, cfg ProjectUpdateRequest) (Updat return v, nil } -// DeleteProject Deletes the specified project. -// You can obtain a `project_id` by listing the projects for your Neon account. -// Deleting a project is a permanent action. -// Deleting a project also deletes endpoints, branches, databases, and users that belong to the project. -func (c Client) DeleteProject(projectID string) (ProjectResponse, error) { - var v ProjectResponse - if err := c.requestHandler(c.baseURL+"/projects/"+projectID, "DELETE", nil, &v); err != nil { - return ProjectResponse{}, err - } - return v, nil -} - // ListProjectOperations Retrieves a list of operations for the specified Neon project. // You can obtain a `project_id` by listing the projects for your Neon account. // The number of operations returned can be large. @@ -368,7 +368,7 @@ func (c Client) ListProjectBranches(projectID string) (BranchesResponse, error) // CreateProjectBranch Creates a branch in the specified project. // You can obtain a `project_id` by listing the projects for your Neon account. // This method does not require a request body, but you can specify one to create a compute endpoint for the branch or to select a non-default parent branch. -// The default behavior is to create a branch from the project's primary branch with no compute endpoint, and the branch name is auto-generated. +// The default behavior is to create a branch from the project's default branch with no compute endpoint, and the branch name is auto-generated. // There is a maximum of one read-write endpoint per branch. // A branch can have multiple read-only endpoints. // For related information, see [Manage branches](https://neon.tech/docs/manage/branches/). @@ -380,11 +380,29 @@ func (c Client) CreateProjectBranch(projectID string, cfg *BranchCreateRequest) return v, nil } +// DeleteProjectBranch Deletes the specified branch from a project, and places +// all compute endpoints into an idle state, breaking existing client connections. +// You can obtain a `project_id` by listing the projects for your Neon account. +// You can obtain a `branch_id` by listing the project's branches. +// For related information, see [Manage branches](https://neon.tech/docs/manage/branches/). +// When a successful response status is received, the compute endpoints are still active, +// and the branch is not yet deleted from storage. +// The deletion occurs after all operations finish. +// You cannot delete a project's root or default branch, and you cannot delete a branch that has a child branch. +// A project must have at least one branch. +func (c Client) DeleteProjectBranch(projectID string, branchID string) (BranchOperations, error) { + var v BranchOperations + if err := c.requestHandler(c.baseURL+"/projects/"+projectID+"/branches/"+branchID, "DELETE", nil, &v); err != nil { + return BranchOperations{}, err + } + return v, nil +} + // GetProjectBranch Retrieves information about the specified branch. // You can obtain a `project_id` by listing the projects for your Neon account. // You can obtain a `branch_id` by listing the project's branches. // A `branch_id` value has a `br-` prefix. -// Each Neon project is initially created with a root and primary branch named `main`. +// Each Neon project is initially created with a root and default branch named `main`. // A project can contain one or more branches. // A parent branch is identified by a `parent_id` value, which is the `id` of the parent branch. // For related information, see [Manage branches](https://neon.tech/docs/manage/branches/). @@ -408,34 +426,41 @@ func (c Client) UpdateProjectBranch(projectID string, branchID string, cfg Branc return v, nil } -// DeleteProjectBranch Deletes the specified branch from a project, and places -// all compute endpoints into an idle state, breaking existing client connections. -// You can obtain a `project_id` by listing the projects for your Neon account. -// You can obtain a `branch_id` by listing the project's branches. -// For related information, see [Manage branches](https://neon.tech/docs/manage/branches/). -// When a successful response status is received, the compute endpoints are still active, -// and the branch is not yet deleted from storage. -// The deletion occurs after all operations finish. -// You cannot delete a project's root or primary branch, and you cannot delete a branch that has a child branch. -// A project must have at least one branch. -func (c Client) DeleteProjectBranch(projectID string, branchID string) (BranchOperations, error) { +// RestoreProjectBranch Restores a branch to an earlier state in its own or another branch's history +func (c Client) RestoreProjectBranch(projectID string, branchID string, cfg BranchRestoreRequest) (BranchOperations, error) { var v BranchOperations - if err := c.requestHandler(c.baseURL+"/projects/"+projectID+"/branches/"+branchID, "DELETE", nil, &v); err != nil { + if err := c.requestHandler(c.baseURL+"/projects/"+projectID+"/branches/"+branchID+"/restore", "POST", cfg, &v); err != nil { return BranchOperations{}, err } return v, nil } -// RestoreProjectBranch Restores a branch to an earlier state in its own or another branch's history -func (c Client) RestoreProjectBranch(projectID string, branchID string, cfg BranchRestoreRequest) (BranchOperations, error) { - var v BranchOperations - if err := c.requestHandler(c.baseURL+"/projects/"+projectID+"/branches/"+branchID+"/restore", "POST", cfg, &v); err != nil { - return BranchOperations{}, err +// GetProjectBranchSchema Retrieves the schema from the specified database. The `lsn` and `timestamp` values cannot be specified at the same time. If both are omitted, the database schema is retrieved from database's head . +func (c Client) GetProjectBranchSchema(projectID string, branchID string, role string, dbName string, lsn *string, timestamp *time.Time) (BranchSchemaResponse, error) { + var ( + queryElements []string + query string + ) + queryElements = append(queryElements, "role="+role) + queryElements = append(queryElements, "db_name="+dbName) + if lsn != nil { + queryElements = append(queryElements, "lsn="+*lsn) + } + if timestamp != nil { + queryElements = append(queryElements, "timestamp="+timestamp.Format(time.RFC3339)) + } + if len(queryElements) > 0 { + query = "?" + strings.Join(queryElements, "&") + } + var v BranchSchemaResponse + if err := c.requestHandler(c.baseURL+"/projects/"+projectID+"/branches/"+branchID+"/schema"+query, "GET", nil, &v); err != nil { + return BranchSchemaResponse{}, err } return v, nil } -// SetPrimaryProjectBranch Sets the specified branch as the project's primary branch. +// SetPrimaryProjectBranch DEPRECATED. Use `/set_as_default` endpoint. +// Sets the specified branch as the project's primary branch. // The primary designation is automatically removed from the previous primary branch. // You can obtain a `project_id` by listing the projects for your Neon account. // You can obtain the `branch_id` by listing the project's branches. @@ -448,6 +473,19 @@ func (c Client) SetPrimaryProjectBranch(projectID string, branchID string) (Bran return v, nil } +// SetDefaultProjectBranch Sets the specified branch as the project's default branch. +// The default designation is automatically removed from the previous default branch. +// You can obtain a `project_id` by listing the projects for your Neon account. +// You can obtain the `branch_id` by listing the project's branches. +// For more information, see [Manage branches](https://neon.tech/docs/manage/branches/). +func (c Client) SetDefaultProjectBranch(projectID string, branchID string) (BranchOperations, error) { + var v BranchOperations + if err := c.requestHandler(c.baseURL+"/projects/"+projectID+"/branches/"+branchID+"/set_as_default", "POST", nil, &v); err != nil { + return BranchOperations{}, err + } + return v, nil +} + // ListProjectBranchEndpoints Retrieves a list of compute endpoints for the specified branch. // Neon permits only one read-write compute endpoint per branch. // A branch can have multiple read-only compute endpoints. @@ -487,6 +525,18 @@ func (c Client) CreateProjectBranchDatabase(projectID string, branchID string, c return v, nil } +// DeleteProjectBranchDatabase Deletes the specified database from the branch. +// You can obtain a `project_id` by listing the projects for your Neon account. +// You can obtain the `branch_id` and `database_name` by listing the branch's databases. +// For related information, see [Manage databases](https://neon.tech/docs/manage/databases/). +func (c Client) DeleteProjectBranchDatabase(projectID string, branchID string, databaseName string) (DatabaseOperations, error) { + var v DatabaseOperations + if err := c.requestHandler(c.baseURL+"/projects/"+projectID+"/branches/"+branchID+"/databases/"+databaseName, "DELETE", nil, &v); err != nil { + return DatabaseOperations{}, err + } + return v, nil +} + // GetProjectBranchDatabase Retrieves information about the specified database. // You can obtain a `project_id` by listing the projects for your Neon account. // You can obtain the `branch_id` and `database_name` by listing the branch's databases. @@ -511,18 +561,6 @@ func (c Client) UpdateProjectBranchDatabase(projectID string, branchID string, d return v, nil } -// DeleteProjectBranchDatabase Deletes the specified database from the branch. -// You can obtain a `project_id` by listing the projects for your Neon account. -// You can obtain the `branch_id` and `database_name` by listing the branch's databases. -// For related information, see [Manage databases](https://neon.tech/docs/manage/databases/). -func (c Client) DeleteProjectBranchDatabase(projectID string, branchID string, databaseName string) (DatabaseOperations, error) { - var v DatabaseOperations - if err := c.requestHandler(c.baseURL+"/projects/"+projectID+"/branches/"+branchID+"/databases/"+databaseName, "DELETE", nil, &v); err != nil { - return DatabaseOperations{}, err - } - return v, nil -} - // ListProjectBranchRoles Retrieves a list of Postgres roles from the specified branch. // You can obtain a `project_id` by listing the projects for your Neon account. // You can obtain the `branch_id` by listing the project's branches. @@ -636,6 +674,22 @@ func (c Client) CreateProjectEndpoint(projectID string, cfg EndpointCreateReques return v, nil } +// DeleteProjectEndpoint Delete the specified compute endpoint. +// A compute endpoint is a Neon compute instance. +// Deleting a compute endpoint drops existing network connections to the compute endpoint. +// The deletion is completed when last operation in the chain finishes successfully. +// You can obtain a `project_id` by listing the projects for your Neon account. +// You can obtain an `endpoint_id` by listing your project's compute endpoints. +// An `endpoint_id` has an `ep-` prefix. +// For information about compute endpoints, see [Manage computes](https://neon.tech/docs/manage/endpoints/). +func (c Client) DeleteProjectEndpoint(projectID string, endpointID string) (EndpointOperations, error) { + var v EndpointOperations + if err := c.requestHandler(c.baseURL+"/projects/"+projectID+"/endpoints/"+endpointID, "DELETE", nil, &v); err != nil { + return EndpointOperations{}, err + } + return v, nil +} + // GetProjectEndpoint Retrieves information about the specified compute endpoint. // A compute endpoint is a Neon compute instance. // You can obtain a `project_id` by listing the projects for your Neon account. @@ -667,22 +721,6 @@ func (c Client) UpdateProjectEndpoint(projectID string, endpointID string, cfg E return v, nil } -// DeleteProjectEndpoint Delete the specified compute endpoint. -// A compute endpoint is a Neon compute instance. -// Deleting a compute endpoint drops existing network connections to the compute endpoint. -// The deletion is completed when last operation in the chain finishes successfully. -// You can obtain a `project_id` by listing the projects for your Neon account. -// You can obtain an `endpoint_id` by listing your project's compute endpoints. -// An `endpoint_id` has an `ep-` prefix. -// For information about compute endpoints, see [Manage computes](https://neon.tech/docs/manage/endpoints/). -func (c Client) DeleteProjectEndpoint(projectID string, endpointID string) (EndpointOperations, error) { - var v EndpointOperations - if err := c.requestHandler(c.baseURL+"/projects/"+projectID+"/endpoints/"+endpointID, "DELETE", nil, &v); err != nil { - return EndpointOperations{}, err - } - return v, nil -} - // StartProjectEndpoint Starts a compute endpoint. The compute endpoint is ready to use // after the last operation in chain finishes successfully. // You can obtain a `project_id` by listing the projects for your Neon account. @@ -723,9 +761,80 @@ func (c Client) RestartProjectEndpoint(projectID string, endpointID string) (End return v, nil } +// GetConsumptionHistoryPerAccount Retrieves consumption metrics for Scale plan accounts. History begins at the time of upgrade. +// Available for Scale plan users only. +func (c Client) GetConsumptionHistoryPerAccount(from time.Time, to time.Time, granularity ConsumptionHistoryGranularity, orgID *string, includeV1Metrics *bool) (ConsumptionHistoryPerAccountResponse, error) { + var ( + queryElements []string + query string + ) + queryElements = append(queryElements, "from="+from.Format(time.RFC3339)) + queryElements = append(queryElements, "to="+to.Format(time.RFC3339)) + queryElements = append(queryElements, "granularity="+string(granularity)) + if orgID != nil { + queryElements = append(queryElements, "org_id="+*orgID) + } + if includeV1Metrics != nil { + queryElements = append(queryElements, "include_v1_metrics="+func(includeV1Metrics bool) string { + if includeV1Metrics { + return "true" + } + return "false" + }(*includeV1Metrics)) + } + if len(queryElements) > 0 { + query = "?" + strings.Join(queryElements, "&") + } + var v ConsumptionHistoryPerAccountResponse + if err := c.requestHandler(c.baseURL+"/consumption_history/account"+query, "GET", nil, &v); err != nil { + return ConsumptionHistoryPerAccountResponse{}, err + } + return v, nil +} + +// GetConsumptionHistoryPerProject Retrieves consumption metrics for Scale plan projects. History begins at the time of upgrade. +// Available for Scale plan users only. +func (c Client) GetConsumptionHistoryPerProject(cursor *string, limit *int, projectIDs []string, from time.Time, to time.Time, granularity ConsumptionHistoryGranularity, orgID *string, includeV1Metrics *bool) (GetConsumptionHistoryPerProjectRespObj, error) { + var ( + queryElements []string + query string + ) + queryElements = append(queryElements, "from="+from.Format(time.RFC3339)) + queryElements = append(queryElements, "to="+to.Format(time.RFC3339)) + queryElements = append(queryElements, "granularity="+string(granularity)) + if cursor != nil { + queryElements = append(queryElements, "cursor="+*cursor) + } + if limit != nil { + queryElements = append(queryElements, "limit="+strconv.FormatInt(int64(*limit), 10)) + } + if len(projectIDs) > 0 { + queryElements = append(queryElements, "project_ids="+strings.Join(projectIDs, ",")) + } + if orgID != nil { + queryElements = append(queryElements, "org_id="+*orgID) + } + if includeV1Metrics != nil { + queryElements = append(queryElements, "include_v1_metrics="+func(includeV1Metrics bool) string { + if includeV1Metrics { + return "true" + } + return "false" + }(*includeV1Metrics)) + } + if len(queryElements) > 0 { + query = "?" + strings.Join(queryElements, "&") + } + var v GetConsumptionHistoryPerProjectRespObj + if err := c.requestHandler(c.baseURL+"/consumption_history/projects"+query, "GET", nil, &v); err != nil { + return GetConsumptionHistoryPerProjectRespObj{}, err + } + return v, nil +} + // ListProjectsConsumption Retrieves consumption metrics for each project for the current billing period. // For usage information, see [Retrieving metrics for all projects](https://neon.tech/docs/guides/partner-billing#retrieving-metrics-for-all-projects). -func (c Client) ListProjectsConsumption(cursor *string, limit *int, from *time.Time, to *time.Time) (ListProjectsConsumptionRespObj, error) { +func (c Client) ListProjectsConsumption(cursor *string, limit *int, from *time.Time, to *time.Time, orgID *string) (ListProjectsConsumptionRespObj, error) { var ( queryElements []string query string @@ -742,6 +851,9 @@ func (c Client) ListProjectsConsumption(cursor *string, limit *int, from *time.T if to != nil { queryElements = append(queryElements, "to="+to.Format(time.RFC3339)) } + if orgID != nil { + queryElements = append(queryElements, "org_id="+*orgID) + } if len(queryElements) > 0 { query = "?" + strings.Join(queryElements, "&") } @@ -761,13 +873,22 @@ func (c Client) GetCurrentUserInfo() (CurrentUserInfoResponse, error) { return v, nil } +// GetCurrentUserOrganizations Retrieves information about the current Neon user's organizations +func (c Client) GetCurrentUserOrganizations() (OrganizationsResponse, error) { + var v OrganizationsResponse + if err := c.requestHandler(c.baseURL+"/users/me/organizations", "GET", nil, &v); err != nil { + return OrganizationsResponse{}, err + } + return v, nil +} + // AllowedIps A list of IP addresses that are allowed to connect to the compute endpoint. // If the list is empty or not set, all IP addresses are allowed. // If protected_branches_only is true, the list will be applied only to protected branches. type AllowedIps struct { // Ips A list of IP addresses that are allowed to connect to the endpoint. Ips *[]string `json:"ips,omitempty"` - // PrimaryBranchOnly If true, the list will be applied only to the primary branch. + // PrimaryBranchOnly If true, the list will be applied only to the default branch. PrimaryBranchOnly *bool `json:"primary_branch_only,omitempty"` // ProtectedBranchesOnly If true, the list will be applied only to protected branches. ProtectedBranchesOnly *bool `json:"protected_branches_only,omitempty"` @@ -818,8 +939,10 @@ type ApiKeysListResponseItem struct { type BillingAccount struct { // AddressCity Billing address city. AddressCity string `json:"address_city"` - // AddressCountry Billing address country. + // AddressCountry Billing address country code defined by ISO 3166-1 alpha-2. AddressCountry string `json:"address_country"` + // AddressCountryName Billing address country name. + AddressCountryName *string `json:"address_country_name,omitempty"` // AddressLine1 Billing address line 1. AddressLine1 string `json:"address_line1"` // AddressLine2 Billing address line 2. @@ -830,18 +953,33 @@ type BillingAccount struct { AddressState string `json:"address_state"` // Email Billing email, to receive emails related to invoices and subscriptions. Email string `json:"email"` + // Name The full name of the individual or entity that owns the billing account. This name appears on invoices. + Name string `json:"name"` // OrbPortalURL Orb user portal url OrbPortalURL *string `json:"orb_portal_url,omitempty"` PaymentSource PaymentSource `json:"payment_source"` // QuotaResetAtLast The last time the quota was reset. Defaults to the date-time the account is created. QuotaResetAtLast time.Time `json:"quota_reset_at_last"` SubscriptionType BillingSubscriptionType `json:"subscription_type"` + // TaxID The tax identification number for the billing account, displayed on invoices. + TaxID *string `json:"tax_id,omitempty"` + // TaxIDType The type of the tax identification number based on the country. + TaxIDType *string `json:"tax_id_type,omitempty"` } // BillingSubscriptionType Type of subscription to Neon Cloud. // Notice that for users without billing account this will be "UNKNOWN" type BillingSubscriptionType string +const ( + BillingSubscriptionTypeUNKNOWN BillingSubscriptionType = "UNKNOWN" + BillingSubscriptionTypeDirectSales BillingSubscriptionType = "direct_sales" + BillingSubscriptionTypeAwsMarketplace BillingSubscriptionType = "aws_marketplace" + BillingSubscriptionTypeFreeV2 BillingSubscriptionType = "free_v2" + BillingSubscriptionTypeLaunch BillingSubscriptionType = "launch" + BillingSubscriptionTypeScale BillingSubscriptionType = "scale" +) + type Branch struct { ActiveTimeSeconds int64 `json:"active_time_seconds"` ComputeTimeSeconds int64 `json:"compute_time_seconds"` @@ -857,6 +995,8 @@ type Branch struct { CreationSource string `json:"creation_source"` CurrentState BranchState `json:"current_state"` DataTransferBytes int64 `json:"data_transfer_bytes"` + // Default Whether the branch is the project's default branch + Default bool `json:"default"` // ID The branch ID. This value is generated when a branch is created. A `branch_id` value has a `br` prefix. For example: `br-small-term-683261`. ID string `json:"id"` // LastResetAt A timestamp indicating when the branch was last reset @@ -872,7 +1012,8 @@ type Branch struct { // ParentTimestamp The point in time on the parent branch from which this branch was created ParentTimestamp *time.Time `json:"parent_timestamp,omitempty"` PendingState *BranchState `json:"pending_state,omitempty"` - // Primary Whether the branch is the project's primary branch + // Primary DEPRECATED. Use `default` field. + // Whether the branch is the project's primary branch Primary bool `json:"primary"` // ProjectID The ID of the project to which the branch belongs ProjectID string `json:"project_id"` @@ -891,7 +1032,7 @@ type BranchCreateRequest struct { type BranchCreateRequestBranch struct { // Name The branch name Name *string `json:"name,omitempty"` - // ParentID The `branch_id` of the parent branch. If omitted or empty, the branch will be created from the project's primary branch. + // ParentID The `branch_id` of the parent branch. If omitted or empty, the branch will be created from the project's default branch. ParentID *string `json:"parent_id,omitempty"` // ParentLsn A Log Sequence Number (LSN) on the parent branch. The branch will be created with data from this LSN. ParentLsn *string `json:"parent_lsn,omitempty"` @@ -934,9 +1075,18 @@ type BranchRestoreRequest struct { SourceTimestamp *time.Time `json:"source_timestamp,omitempty"` } +type BranchSchemaResponse struct { + Sql *string `json:"sql,omitempty"` +} + // BranchState The branch state type BranchState string +const ( + BranchStateInit BranchState = "init" + BranchStateReady BranchState = "ready" +) + type BranchUpdateRequest struct { Branch BranchUpdateRequestBranch `json:"branch"` } @@ -978,11 +1128,47 @@ type ConnectionURIResponse struct { } type ConnectionURIsOptionalResponse struct { - ConnectionUris *[]ConnectionDetails `json:"connection_uris,omitempty"` + ConnectionURIs *[]ConnectionDetails `json:"connection_uris,omitempty"` } type ConnectionURIsResponse struct { - ConnectionUris []ConnectionDetails `json:"connection_uris"` + ConnectionURIs []ConnectionDetails `json:"connection_uris"` +} + +type ConsumptionHistoryGranularity string + +const ( + ConsumptionHistoryGranularityDaily ConsumptionHistoryGranularity = "daily" + ConsumptionHistoryGranularityMonthly ConsumptionHistoryGranularity = "monthly" + ConsumptionHistoryGranularityHourly ConsumptionHistoryGranularity = "hourly" +) + +type ConsumptionHistoryPerAccountResponse struct { + Periods []ConsumptionHistoryPerPeriod `json:"periods"` +} + +type ConsumptionHistoryPerPeriod struct { + Consumption []ConsumptionHistoryPerTimeframe `json:"consumption"` + PeriodID string `json:"period_id"` +} + +type ConsumptionHistoryPerProject struct { + Periods []ConsumptionHistoryPerPeriod `json:"periods"` + ProjectID string `json:"project_id"` +} + +type ConsumptionHistoryPerProjectResponse struct { + Projects []ConsumptionHistoryPerProject `json:"projects"` +} + +type ConsumptionHistoryPerTimeframe struct { + ActiveTimeSeconds int `json:"active_time_seconds"` + ComputeTimeSeconds int `json:"compute_time_seconds"` + DataStorageBytesHour *int `json:"data_storage_bytes_hour,omitempty"` + SyntheticStorageSizeBytes int `json:"synthetic_storage_size_bytes"` + TimeframeEnd time.Time `json:"timeframe_end"` + TimeframeStart time.Time `json:"timeframe_start"` + WrittenDataBytes int `json:"written_data_bytes"` } type CreatedBranch struct { @@ -1169,6 +1355,10 @@ type EndpointOperations struct { // EndpointPoolerMode The connection pooler mode. Neon supports PgBouncer in `transaction` mode only. type EndpointPoolerMode string +const ( + EndpointPoolerModeTransaction EndpointPoolerMode = "transaction" +) + type EndpointResponse struct { Endpoint Endpoint `json:"endpoint"` } @@ -1182,10 +1372,21 @@ type EndpointSettingsData struct { // EndpointState The state of the compute endpoint type EndpointState string +const ( + EndpointStateInit EndpointState = "init" + EndpointStateActive EndpointState = "active" + EndpointStateIdle EndpointState = "idle" +) + // EndpointType The compute endpoint type. Either `read_write` or `read_only`. // The `read_only` compute endpoint type is not yet supported. type EndpointType string +const ( + EndpointTypeReadOnly EndpointType = "read_only" + EndpointTypeReadWrite EndpointType = "read_write" +) + type EndpointUpdateRequest struct { Endpoint EndpointUpdateRequestEndpoint `json:"endpoint"` } @@ -1215,6 +1416,11 @@ type EndpointsResponse struct { Endpoints []Endpoint `json:"endpoints"` } +type GetConsumptionHistoryPerProjectRespObj struct { + ConsumptionHistoryPerProjectResponse + PaginationResponse +} + type GrantPermissionToProjectRequest struct { Email string `json:"email"` } @@ -1222,6 +1428,13 @@ type GrantPermissionToProjectRequest struct { // IdentityProviderId Identity provider id from keycloak type IdentityProviderId string +const ( + IdentityProviderIdGithub IdentityProviderId = "github" + IdentityProviderIdGoogle IdentityProviderId = "google" + IdentityProviderIdHasura IdentityProviderId = "hasura" + IdentityProviderIdKeycloak IdentityProviderId = "keycloak" +) + type ListOperations struct { OperationsResponse PaginationResponse @@ -1270,6 +1483,26 @@ type Operation struct { // OperationAction The action performed by the operation type OperationAction string +const ( + OperationActionCheckAvailability OperationAction = "check_availability" + OperationActionTenantReattach OperationAction = "tenant_reattach" + OperationActionDisableMaintenance OperationAction = "disable_maintenance" + OperationActionSwitchPageserver OperationAction = "switch_pageserver" + OperationActionCreateTimeline OperationAction = "create_timeline" + OperationActionDeleteTimeline OperationAction = "delete_timeline" + OperationActionSuspendCompute OperationAction = "suspend_compute" + OperationActionApplyConfig OperationAction = "apply_config" + OperationActionCreateBranch OperationAction = "create_branch" + OperationActionTenantIgnore OperationAction = "tenant_ignore" + OperationActionApplyStorageConfig OperationAction = "apply_storage_config" + OperationActionCreateCompute OperationAction = "create_compute" + OperationActionStartCompute OperationAction = "start_compute" + OperationActionTenantAttach OperationAction = "tenant_attach" + OperationActionTenantDetach OperationAction = "tenant_detach" + OperationActionReplaceSafekeeper OperationAction = "replace_safekeeper" + OperationActionPrepareSecondaryPageserver OperationAction = "prepare_secondary_pageserver" +) + type OperationResponse struct { Operation Operation `json:"operation"` } @@ -1277,10 +1510,35 @@ type OperationResponse struct { // OperationStatus The status of the operation type OperationStatus string +const ( + OperationStatusScheduling OperationStatus = "scheduling" + OperationStatusRunning OperationStatus = "running" + OperationStatusFinished OperationStatus = "finished" + OperationStatusFailed OperationStatus = "failed" + OperationStatusError OperationStatus = "error" + OperationStatusCancelling OperationStatus = "cancelling" + OperationStatusCancelled OperationStatus = "cancelled" + OperationStatusSkipped OperationStatus = "skipped" +) + type OperationsResponse struct { Operations []Operation `json:"operations"` } +type Organization struct { + // CreatedAt A timestamp indicting when the organization was created + CreatedAt time.Time `json:"created_at"` + Handle string `json:"handle"` + ID string `json:"id"` + Name string `json:"name"` + // UpdatedAt A timestamp indicating when the organization was updated + UpdatedAt time.Time `json:"updated_at"` +} + +type OrganizationsResponse struct { + Organizations []Organization `json:"organizations"` +} + // Pagination Cursor based pagination is used. The user must pass the cursor as is to the backend. // For more information about cursor based pagination, see // https://learn.microsoft.com/en-us/ef/core/querying/pagination#keyset-pagination @@ -1604,6 +1862,11 @@ type ProjectsResponse struct { // Specify the `k8s-neonvm` provisioner to create a compute endpoint that supports Autoscaling. type Provisioner string +const ( + ProvisionerK8sNeonvm Provisioner = "k8s-neonvm" + ProvisionerK8sPod Provisioner = "k8s-pod" +) + type Role struct { // BranchID The ID of the branch to which the role belongs BranchID string `json:"branch_id"` diff --git a/sdk_test.go b/sdk_test.go index ff66c66..7d0d69c 100644 --- a/sdk_test.go +++ b/sdk_test.go @@ -671,49 +671,40 @@ func Test_client_GetProjectOperation(t *testing.T) { } } -func Test_client_ListProjects(t *testing.T) { - deserializeResp := func(s string) ListProjectsRespObj { - var v ListProjectsRespObj +func Test_client_CreateProject(t *testing.T) { + deserializeResp := func(s string) CreatedProject { + var v CreatedProject if err := json.Unmarshal([]byte(s), &v); err != nil { panic(err) } return v } type args struct { - cursor *string - limit *int - search *string - orgID *string + cfg ProjectCreateRequest } tests := []struct { name string args args apiKey string - want ListProjectsRespObj + want CreatedProject wantErr bool }{ { name: "happy path", args: args{ - cursor: createPointer("foo"), - limit: createPointer(1), - search: createPointer("foo"), - orgID: createPointer("foo"), + cfg: ProjectCreateRequest{}, }, apiKey: "foo", - want: deserializeResp(endpointResponseExamples["/projects"]["GET"].Content), + want: deserializeResp(endpointResponseExamples["/projects"]["POST"].Content), wantErr: false, }, { name: "unhappy path", args: args{ - cursor: createPointer("foo"), - limit: createPointer(1), - search: createPointer("foo"), - orgID: createPointer("foo"), + cfg: ProjectCreateRequest{}, }, apiKey: "invalidApiKey", - want: ListProjectsRespObj{}, + want: CreatedProject{}, wantErr: true, }, } @@ -724,53 +715,62 @@ func Test_client_ListProjects(t *testing.T) { if err != nil { panic(err) } - got, err := c.ListProjects(tt.args.cursor, tt.args.limit, tt.args.search, tt.args.orgID) + got, err := c.CreateProject(tt.args.cfg) if (err != nil) != tt.wantErr { - t.Errorf("ListProjects() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("CreateProject() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("ListProjects() got = %v, want %v", got, tt.want) + t.Errorf("CreateProject() got = %v, want %v", got, tt.want) } }, ) } } -func Test_client_CreateProject(t *testing.T) { - deserializeResp := func(s string) CreatedProject { - var v CreatedProject +func Test_client_ListProjects(t *testing.T) { + deserializeResp := func(s string) ListProjectsRespObj { + var v ListProjectsRespObj if err := json.Unmarshal([]byte(s), &v); err != nil { panic(err) } return v } type args struct { - cfg ProjectCreateRequest + cursor *string + limit *int + search *string + orgID *string } tests := []struct { name string args args apiKey string - want CreatedProject + want ListProjectsRespObj wantErr bool }{ { name: "happy path", args: args{ - cfg: ProjectCreateRequest{}, + cursor: createPointer("foo"), + limit: createPointer(1), + search: createPointer("foo"), + orgID: createPointer("foo"), }, apiKey: "foo", - want: deserializeResp(endpointResponseExamples["/projects"]["POST"].Content), + want: deserializeResp(endpointResponseExamples["/projects"]["GET"].Content), wantErr: false, }, { name: "unhappy path", args: args{ - cfg: ProjectCreateRequest{}, + cursor: createPointer("foo"), + limit: createPointer(1), + search: createPointer("foo"), + orgID: createPointer("foo"), }, apiKey: "invalidApiKey", - want: CreatedProject{}, + want: ListProjectsRespObj{}, wantErr: true, }, } @@ -781,13 +781,13 @@ func Test_client_CreateProject(t *testing.T) { if err != nil { panic(err) } - got, err := c.CreateProject(tt.args.cfg) + got, err := c.ListProjects(tt.args.cursor, tt.args.limit, tt.args.search, tt.args.orgID) if (err != nil) != tt.wantErr { - t.Errorf("CreateProject() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("ListProjects() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("CreateProject() got = %v, want %v", got, tt.want) + t.Errorf("ListProjects() got = %v, want %v", got, tt.want) } }, ) @@ -857,7 +857,7 @@ func Test_client_ListSharedProjects(t *testing.T) { } } -func Test_client_GetProject(t *testing.T) { +func Test_client_DeleteProject(t *testing.T) { deserializeResp := func(s string) ProjectResponse { var v ProjectResponse if err := json.Unmarshal([]byte(s), &v); err != nil { @@ -881,7 +881,7 @@ func Test_client_GetProject(t *testing.T) { projectID: "foo", }, apiKey: "foo", - want: deserializeResp(endpointResponseExamples["/projects/{project_id}"]["GET"].Content), + want: deserializeResp(endpointResponseExamples["/projects/{project_id}"]["DELETE"].Content), wantErr: false, }, { @@ -901,22 +901,22 @@ func Test_client_GetProject(t *testing.T) { if err != nil { panic(err) } - got, err := c.GetProject(tt.args.projectID) + got, err := c.DeleteProject(tt.args.projectID) if (err != nil) != tt.wantErr { - t.Errorf("GetProject() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("DeleteProject() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("GetProject() got = %v, want %v", got, tt.want) + t.Errorf("DeleteProject() got = %v, want %v", got, tt.want) } }, ) } } -func Test_client_UpdateProject(t *testing.T) { - deserializeResp := func(s string) UpdateProjectRespObj { - var v UpdateProjectRespObj +func Test_client_GetProject(t *testing.T) { + deserializeResp := func(s string) ProjectResponse { + var v ProjectResponse if err := json.Unmarshal([]byte(s), &v); err != nil { panic(err) } @@ -924,33 +924,30 @@ func Test_client_UpdateProject(t *testing.T) { } type args struct { projectID string - cfg ProjectUpdateRequest } tests := []struct { name string args args apiKey string - want UpdateProjectRespObj + want ProjectResponse wantErr bool }{ { name: "happy path", args: args{ projectID: "foo", - cfg: ProjectUpdateRequest{}, }, apiKey: "foo", - want: deserializeResp(endpointResponseExamples["/projects/{project_id}"]["PATCH"].Content), + want: deserializeResp(endpointResponseExamples["/projects/{project_id}"]["GET"].Content), wantErr: false, }, { name: "unhappy path", args: args{ projectID: "foo", - cfg: ProjectUpdateRequest{}, }, apiKey: "invalidApiKey", - want: UpdateProjectRespObj{}, + want: ProjectResponse{}, wantErr: true, }, } @@ -961,22 +958,22 @@ func Test_client_UpdateProject(t *testing.T) { if err != nil { panic(err) } - got, err := c.UpdateProject(tt.args.projectID, tt.args.cfg) + got, err := c.GetProject(tt.args.projectID) if (err != nil) != tt.wantErr { - t.Errorf("UpdateProject() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("GetProject() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("UpdateProject() got = %v, want %v", got, tt.want) + t.Errorf("GetProject() got = %v, want %v", got, tt.want) } }, ) } } -func Test_client_DeleteProject(t *testing.T) { - deserializeResp := func(s string) ProjectResponse { - var v ProjectResponse +func Test_client_UpdateProject(t *testing.T) { + deserializeResp := func(s string) UpdateProjectRespObj { + var v UpdateProjectRespObj if err := json.Unmarshal([]byte(s), &v); err != nil { panic(err) } @@ -984,30 +981,33 @@ func Test_client_DeleteProject(t *testing.T) { } type args struct { projectID string + cfg ProjectUpdateRequest } tests := []struct { name string args args apiKey string - want ProjectResponse + want UpdateProjectRespObj wantErr bool }{ { name: "happy path", args: args{ projectID: "foo", + cfg: ProjectUpdateRequest{}, }, apiKey: "foo", - want: deserializeResp(endpointResponseExamples["/projects/{project_id}"]["DELETE"].Content), + want: deserializeResp(endpointResponseExamples["/projects/{project_id}"]["PATCH"].Content), wantErr: false, }, { name: "unhappy path", args: args{ projectID: "foo", + cfg: ProjectUpdateRequest{}, }, apiKey: "invalidApiKey", - want: ProjectResponse{}, + want: UpdateProjectRespObj{}, wantErr: true, }, } @@ -1018,13 +1018,13 @@ func Test_client_DeleteProject(t *testing.T) { if err != nil { panic(err) } - got, err := c.DeleteProject(tt.args.projectID) + got, err := c.UpdateProject(tt.args.projectID, tt.args.cfg) if (err != nil) != tt.wantErr { - t.Errorf("DeleteProject() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("UpdateProject() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("DeleteProject() got = %v, want %v", got, tt.want) + t.Errorf("UpdateProject() got = %v, want %v", got, tt.want) } }, ) @@ -1283,6 +1283,66 @@ func Test_client_CreateProjectBranch(t *testing.T) { } } +func Test_client_DeleteProjectBranch(t *testing.T) { + deserializeResp := func(s string) BranchOperations { + var v BranchOperations + if err := json.Unmarshal([]byte(s), &v); err != nil { + panic(err) + } + return v + } + type args struct { + projectID string + branchID string + } + tests := []struct { + name string + args args + apiKey string + want BranchOperations + wantErr bool + }{ + { + name: "happy path", + args: args{ + projectID: "foo", + branchID: "foo", + }, + apiKey: "foo", + want: deserializeResp(endpointResponseExamples["/projects/{project_id}/branches/{branch_id}"]["DELETE"].Content), + wantErr: false, + }, + { + name: "unhappy path", + args: args{ + projectID: "foo", + branchID: "foo", + }, + apiKey: "invalidApiKey", + want: BranchOperations{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + c, err := NewClient(Config{tt.apiKey, NewMockHTTPClient()}) + if err != nil { + panic(err) + } + got, err := c.DeleteProjectBranch(tt.args.projectID, tt.args.branchID) + if (err != nil) != tt.wantErr { + t.Errorf("DeleteProjectBranch() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("DeleteProjectBranch() got = %v, want %v", got, tt.want) + } + }, + ) + } +} + func Test_client_GetProjectBranch(t *testing.T) { deserializeResp := func(s string) BranchResponse { var v BranchResponse @@ -1406,7 +1466,7 @@ func Test_client_UpdateProjectBranch(t *testing.T) { } } -func Test_client_DeleteProjectBranch(t *testing.T) { +func Test_client_RestoreProjectBranch(t *testing.T) { deserializeResp := func(s string) BranchOperations { var v BranchOperations if err := json.Unmarshal([]byte(s), &v); err != nil { @@ -1417,6 +1477,7 @@ func Test_client_DeleteProjectBranch(t *testing.T) { type args struct { projectID string branchID string + cfg BranchRestoreRequest } tests := []struct { name string @@ -1430,9 +1491,10 @@ func Test_client_DeleteProjectBranch(t *testing.T) { args: args{ projectID: "foo", branchID: "foo", + cfg: BranchRestoreRequest{}, }, apiKey: "foo", - want: deserializeResp(endpointResponseExamples["/projects/{project_id}/branches/{branch_id}"]["DELETE"].Content), + want: deserializeResp(endpointResponseExamples["/projects/{project_id}/branches/{branch_id}/restore"]["POST"].Content), wantErr: false, }, { @@ -1440,6 +1502,7 @@ func Test_client_DeleteProjectBranch(t *testing.T) { args: args{ projectID: "foo", branchID: "foo", + cfg: BranchRestoreRequest{}, }, apiKey: "invalidApiKey", want: BranchOperations{}, @@ -1453,22 +1516,22 @@ func Test_client_DeleteProjectBranch(t *testing.T) { if err != nil { panic(err) } - got, err := c.DeleteProjectBranch(tt.args.projectID, tt.args.branchID) + got, err := c.RestoreProjectBranch(tt.args.projectID, tt.args.branchID, tt.args.cfg) if (err != nil) != tt.wantErr { - t.Errorf("DeleteProjectBranch() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("RestoreProjectBranch() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("DeleteProjectBranch() got = %v, want %v", got, tt.want) + t.Errorf("RestoreProjectBranch() got = %v, want %v", got, tt.want) } }, ) } } -func Test_client_RestoreProjectBranch(t *testing.T) { - deserializeResp := func(s string) BranchOperations { - var v BranchOperations +func Test_client_GetProjectBranchSchema(t *testing.T) { + deserializeResp := func(s string) BranchSchemaResponse { + var v BranchSchemaResponse if err := json.Unmarshal([]byte(s), &v); err != nil { panic(err) } @@ -1477,13 +1540,16 @@ func Test_client_RestoreProjectBranch(t *testing.T) { type args struct { projectID string branchID string - cfg BranchRestoreRequest + role string + dbName string + lsn *string + timestamp *time.Time } tests := []struct { name string args args apiKey string - want BranchOperations + want BranchSchemaResponse wantErr bool }{ { @@ -1491,10 +1557,13 @@ func Test_client_RestoreProjectBranch(t *testing.T) { args: args{ projectID: "foo", branchID: "foo", - cfg: BranchRestoreRequest{}, + role: "foo", + dbName: "foo", + lsn: createPointer("foo"), + timestamp: createPointer(time.Time{}), }, apiKey: "foo", - want: deserializeResp(endpointResponseExamples["/projects/{project_id}/branches/{branch_id}/restore"]["POST"].Content), + want: deserializeResp(endpointResponseExamples["/projects/{project_id}/branches/{branch_id}/schema"]["GET"].Content), wantErr: false, }, { @@ -1502,10 +1571,13 @@ func Test_client_RestoreProjectBranch(t *testing.T) { args: args{ projectID: "foo", branchID: "foo", - cfg: BranchRestoreRequest{}, + role: "foo", + dbName: "foo", + lsn: createPointer("foo"), + timestamp: createPointer(time.Time{}), }, apiKey: "invalidApiKey", - want: BranchOperations{}, + want: BranchSchemaResponse{}, wantErr: true, }, } @@ -1516,13 +1588,13 @@ func Test_client_RestoreProjectBranch(t *testing.T) { if err != nil { panic(err) } - got, err := c.RestoreProjectBranch(tt.args.projectID, tt.args.branchID, tt.args.cfg) + got, err := c.GetProjectBranchSchema(tt.args.projectID, tt.args.branchID, tt.args.role, tt.args.dbName, tt.args.lsn, tt.args.timestamp) if (err != nil) != tt.wantErr { - t.Errorf("RestoreProjectBranch() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("GetProjectBranchSchema() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("RestoreProjectBranch() got = %v, want %v", got, tt.want) + t.Errorf("GetProjectBranchSchema() got = %v, want %v", got, tt.want) } }, ) @@ -1589,6 +1661,66 @@ func Test_client_SetPrimaryProjectBranch(t *testing.T) { } } +func Test_client_SetDefaultProjectBranch(t *testing.T) { + deserializeResp := func(s string) BranchOperations { + var v BranchOperations + if err := json.Unmarshal([]byte(s), &v); err != nil { + panic(err) + } + return v + } + type args struct { + projectID string + branchID string + } + tests := []struct { + name string + args args + apiKey string + want BranchOperations + wantErr bool + }{ + { + name: "happy path", + args: args{ + projectID: "foo", + branchID: "foo", + }, + apiKey: "foo", + want: deserializeResp(endpointResponseExamples["/projects/{project_id}/branches/{branch_id}/set_as_default"]["POST"].Content), + wantErr: false, + }, + { + name: "unhappy path", + args: args{ + projectID: "foo", + branchID: "foo", + }, + apiKey: "invalidApiKey", + want: BranchOperations{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + c, err := NewClient(Config{tt.apiKey, NewMockHTTPClient()}) + if err != nil { + panic(err) + } + got, err := c.SetDefaultProjectBranch(tt.args.projectID, tt.args.branchID) + if (err != nil) != tt.wantErr { + t.Errorf("SetDefaultProjectBranch() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SetDefaultProjectBranch() got = %v, want %v", got, tt.want) + } + }, + ) + } +} + func Test_client_ListProjectBranchEndpoints(t *testing.T) { deserializeResp := func(s string) EndpointsResponse { var v EndpointsResponse @@ -1772,9 +1904,9 @@ func Test_client_CreateProjectBranchDatabase(t *testing.T) { } } -func Test_client_GetProjectBranchDatabase(t *testing.T) { - deserializeResp := func(s string) DatabaseResponse { - var v DatabaseResponse +func Test_client_DeleteProjectBranchDatabase(t *testing.T) { + deserializeResp := func(s string) DatabaseOperations { + var v DatabaseOperations if err := json.Unmarshal([]byte(s), &v); err != nil { panic(err) } @@ -1789,7 +1921,7 @@ func Test_client_GetProjectBranchDatabase(t *testing.T) { name string args args apiKey string - want DatabaseResponse + want DatabaseOperations wantErr bool }{ { @@ -1800,7 +1932,7 @@ func Test_client_GetProjectBranchDatabase(t *testing.T) { databaseName: "foo", }, apiKey: "foo", - want: deserializeResp(endpointResponseExamples["/projects/{project_id}/branches/{branch_id}/databases/{database_name}"]["GET"].Content), + want: deserializeResp(endpointResponseExamples["/projects/{project_id}/branches/{branch_id}/databases/{database_name}"]["DELETE"].Content), wantErr: false, }, { @@ -1811,7 +1943,7 @@ func Test_client_GetProjectBranchDatabase(t *testing.T) { databaseName: "foo", }, apiKey: "invalidApiKey", - want: DatabaseResponse{}, + want: DatabaseOperations{}, wantErr: true, }, } @@ -1822,22 +1954,22 @@ func Test_client_GetProjectBranchDatabase(t *testing.T) { if err != nil { panic(err) } - got, err := c.GetProjectBranchDatabase(tt.args.projectID, tt.args.branchID, tt.args.databaseName) + got, err := c.DeleteProjectBranchDatabase(tt.args.projectID, tt.args.branchID, tt.args.databaseName) if (err != nil) != tt.wantErr { - t.Errorf("GetProjectBranchDatabase() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("DeleteProjectBranchDatabase() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("GetProjectBranchDatabase() got = %v, want %v", got, tt.want) + t.Errorf("DeleteProjectBranchDatabase() got = %v, want %v", got, tt.want) } }, ) } } -func Test_client_UpdateProjectBranchDatabase(t *testing.T) { - deserializeResp := func(s string) DatabaseOperations { - var v DatabaseOperations +func Test_client_GetProjectBranchDatabase(t *testing.T) { + deserializeResp := func(s string) DatabaseResponse { + var v DatabaseResponse if err := json.Unmarshal([]byte(s), &v); err != nil { panic(err) } @@ -1847,13 +1979,12 @@ func Test_client_UpdateProjectBranchDatabase(t *testing.T) { projectID string branchID string databaseName string - cfg DatabaseUpdateRequest } tests := []struct { name string args args apiKey string - want DatabaseOperations + want DatabaseResponse wantErr bool }{ { @@ -1862,10 +1993,9 @@ func Test_client_UpdateProjectBranchDatabase(t *testing.T) { projectID: "foo", branchID: "foo", databaseName: "foo", - cfg: DatabaseUpdateRequest{}, }, apiKey: "foo", - want: deserializeResp(endpointResponseExamples["/projects/{project_id}/branches/{branch_id}/databases/{database_name}"]["PATCH"].Content), + want: deserializeResp(endpointResponseExamples["/projects/{project_id}/branches/{branch_id}/databases/{database_name}"]["GET"].Content), wantErr: false, }, { @@ -1874,10 +2004,9 @@ func Test_client_UpdateProjectBranchDatabase(t *testing.T) { projectID: "foo", branchID: "foo", databaseName: "foo", - cfg: DatabaseUpdateRequest{}, }, apiKey: "invalidApiKey", - want: DatabaseOperations{}, + want: DatabaseResponse{}, wantErr: true, }, } @@ -1888,20 +2017,20 @@ func Test_client_UpdateProjectBranchDatabase(t *testing.T) { if err != nil { panic(err) } - got, err := c.UpdateProjectBranchDatabase(tt.args.projectID, tt.args.branchID, tt.args.databaseName, tt.args.cfg) + got, err := c.GetProjectBranchDatabase(tt.args.projectID, tt.args.branchID, tt.args.databaseName) if (err != nil) != tt.wantErr { - t.Errorf("UpdateProjectBranchDatabase() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("GetProjectBranchDatabase() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("UpdateProjectBranchDatabase() got = %v, want %v", got, tt.want) + t.Errorf("GetProjectBranchDatabase() got = %v, want %v", got, tt.want) } }, ) } } -func Test_client_DeleteProjectBranchDatabase(t *testing.T) { +func Test_client_UpdateProjectBranchDatabase(t *testing.T) { deserializeResp := func(s string) DatabaseOperations { var v DatabaseOperations if err := json.Unmarshal([]byte(s), &v); err != nil { @@ -1913,6 +2042,7 @@ func Test_client_DeleteProjectBranchDatabase(t *testing.T) { projectID string branchID string databaseName string + cfg DatabaseUpdateRequest } tests := []struct { name string @@ -1927,9 +2057,10 @@ func Test_client_DeleteProjectBranchDatabase(t *testing.T) { projectID: "foo", branchID: "foo", databaseName: "foo", + cfg: DatabaseUpdateRequest{}, }, apiKey: "foo", - want: deserializeResp(endpointResponseExamples["/projects/{project_id}/branches/{branch_id}/databases/{database_name}"]["DELETE"].Content), + want: deserializeResp(endpointResponseExamples["/projects/{project_id}/branches/{branch_id}/databases/{database_name}"]["PATCH"].Content), wantErr: false, }, { @@ -1938,6 +2069,7 @@ func Test_client_DeleteProjectBranchDatabase(t *testing.T) { projectID: "foo", branchID: "foo", databaseName: "foo", + cfg: DatabaseUpdateRequest{}, }, apiKey: "invalidApiKey", want: DatabaseOperations{}, @@ -1951,13 +2083,13 @@ func Test_client_DeleteProjectBranchDatabase(t *testing.T) { if err != nil { panic(err) } - got, err := c.DeleteProjectBranchDatabase(tt.args.projectID, tt.args.branchID, tt.args.databaseName) + got, err := c.UpdateProjectBranchDatabase(tt.args.projectID, tt.args.branchID, tt.args.databaseName, tt.args.cfg) if (err != nil) != tt.wantErr { - t.Errorf("DeleteProjectBranchDatabase() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("UpdateProjectBranchDatabase() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("DeleteProjectBranchDatabase() got = %v, want %v", got, tt.want) + t.Errorf("UpdateProjectBranchDatabase() got = %v, want %v", got, tt.want) } }, ) @@ -2456,9 +2588,9 @@ func Test_client_CreateProjectEndpoint(t *testing.T) { } } -func Test_client_GetProjectEndpoint(t *testing.T) { - deserializeResp := func(s string) EndpointResponse { - var v EndpointResponse +func Test_client_DeleteProjectEndpoint(t *testing.T) { + deserializeResp := func(s string) EndpointOperations { + var v EndpointOperations if err := json.Unmarshal([]byte(s), &v); err != nil { panic(err) } @@ -2472,7 +2604,7 @@ func Test_client_GetProjectEndpoint(t *testing.T) { name string args args apiKey string - want EndpointResponse + want EndpointOperations wantErr bool }{ { @@ -2482,7 +2614,7 @@ func Test_client_GetProjectEndpoint(t *testing.T) { endpointID: "foo", }, apiKey: "foo", - want: deserializeResp(endpointResponseExamples["/projects/{project_id}/endpoints/{endpoint_id}"]["GET"].Content), + want: deserializeResp(endpointResponseExamples["/projects/{project_id}/endpoints/{endpoint_id}"]["DELETE"].Content), wantErr: false, }, { @@ -2492,7 +2624,7 @@ func Test_client_GetProjectEndpoint(t *testing.T) { endpointID: "foo", }, apiKey: "invalidApiKey", - want: EndpointResponse{}, + want: EndpointOperations{}, wantErr: true, }, } @@ -2503,22 +2635,22 @@ func Test_client_GetProjectEndpoint(t *testing.T) { if err != nil { panic(err) } - got, err := c.GetProjectEndpoint(tt.args.projectID, tt.args.endpointID) + got, err := c.DeleteProjectEndpoint(tt.args.projectID, tt.args.endpointID) if (err != nil) != tt.wantErr { - t.Errorf("GetProjectEndpoint() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("DeleteProjectEndpoint() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("GetProjectEndpoint() got = %v, want %v", got, tt.want) + t.Errorf("DeleteProjectEndpoint() got = %v, want %v", got, tt.want) } }, ) } } -func Test_client_UpdateProjectEndpoint(t *testing.T) { - deserializeResp := func(s string) EndpointOperations { - var v EndpointOperations +func Test_client_GetProjectEndpoint(t *testing.T) { + deserializeResp := func(s string) EndpointResponse { + var v EndpointResponse if err := json.Unmarshal([]byte(s), &v); err != nil { panic(err) } @@ -2527,13 +2659,12 @@ func Test_client_UpdateProjectEndpoint(t *testing.T) { type args struct { projectID string endpointID string - cfg EndpointUpdateRequest } tests := []struct { name string args args apiKey string - want EndpointOperations + want EndpointResponse wantErr bool }{ { @@ -2541,10 +2672,9 @@ func Test_client_UpdateProjectEndpoint(t *testing.T) { args: args{ projectID: "foo", endpointID: "foo", - cfg: EndpointUpdateRequest{}, }, apiKey: "foo", - want: deserializeResp(endpointResponseExamples["/projects/{project_id}/endpoints/{endpoint_id}"]["PATCH"].Content), + want: deserializeResp(endpointResponseExamples["/projects/{project_id}/endpoints/{endpoint_id}"]["GET"].Content), wantErr: false, }, { @@ -2552,10 +2682,9 @@ func Test_client_UpdateProjectEndpoint(t *testing.T) { args: args{ projectID: "foo", endpointID: "foo", - cfg: EndpointUpdateRequest{}, }, apiKey: "invalidApiKey", - want: EndpointOperations{}, + want: EndpointResponse{}, wantErr: true, }, } @@ -2566,20 +2695,20 @@ func Test_client_UpdateProjectEndpoint(t *testing.T) { if err != nil { panic(err) } - got, err := c.UpdateProjectEndpoint(tt.args.projectID, tt.args.endpointID, tt.args.cfg) + got, err := c.GetProjectEndpoint(tt.args.projectID, tt.args.endpointID) if (err != nil) != tt.wantErr { - t.Errorf("UpdateProjectEndpoint() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("GetProjectEndpoint() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("UpdateProjectEndpoint() got = %v, want %v", got, tt.want) + t.Errorf("GetProjectEndpoint() got = %v, want %v", got, tt.want) } }, ) } } -func Test_client_DeleteProjectEndpoint(t *testing.T) { +func Test_client_UpdateProjectEndpoint(t *testing.T) { deserializeResp := func(s string) EndpointOperations { var v EndpointOperations if err := json.Unmarshal([]byte(s), &v); err != nil { @@ -2590,6 +2719,7 @@ func Test_client_DeleteProjectEndpoint(t *testing.T) { type args struct { projectID string endpointID string + cfg EndpointUpdateRequest } tests := []struct { name string @@ -2603,9 +2733,10 @@ func Test_client_DeleteProjectEndpoint(t *testing.T) { args: args{ projectID: "foo", endpointID: "foo", + cfg: EndpointUpdateRequest{}, }, apiKey: "foo", - want: deserializeResp(endpointResponseExamples["/projects/{project_id}/endpoints/{endpoint_id}"]["DELETE"].Content), + want: deserializeResp(endpointResponseExamples["/projects/{project_id}/endpoints/{endpoint_id}"]["PATCH"].Content), wantErr: false, }, { @@ -2613,6 +2744,7 @@ func Test_client_DeleteProjectEndpoint(t *testing.T) { args: args{ projectID: "foo", endpointID: "foo", + cfg: EndpointUpdateRequest{}, }, apiKey: "invalidApiKey", want: EndpointOperations{}, @@ -2626,13 +2758,13 @@ func Test_client_DeleteProjectEndpoint(t *testing.T) { if err != nil { panic(err) } - got, err := c.DeleteProjectEndpoint(tt.args.projectID, tt.args.endpointID) + got, err := c.UpdateProjectEndpoint(tt.args.projectID, tt.args.endpointID, tt.args.cfg) if (err != nil) != tt.wantErr { - t.Errorf("DeleteProjectEndpoint() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("UpdateProjectEndpoint() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("DeleteProjectEndpoint() got = %v, want %v", got, tt.want) + t.Errorf("UpdateProjectEndpoint() got = %v, want %v", got, tt.want) } }, ) @@ -2819,6 +2951,153 @@ func Test_client_RestartProjectEndpoint(t *testing.T) { } } +func Test_client_GetConsumptionHistoryPerAccount(t *testing.T) { + deserializeResp := func(s string) ConsumptionHistoryPerAccountResponse { + var v ConsumptionHistoryPerAccountResponse + if err := json.Unmarshal([]byte(s), &v); err != nil { + panic(err) + } + return v + } + type args struct { + from time.Time + to time.Time + granularity ConsumptionHistoryGranularity + orgID *string + includeV1Metrics *bool + } + tests := []struct { + name string + args args + apiKey string + want ConsumptionHistoryPerAccountResponse + wantErr bool + }{ + { + name: "happy path", + args: args{ + from: time.Time{}, + to: time.Time{}, + granularity: "foo", + orgID: createPointer("foo"), + includeV1Metrics: createPointer(true), + }, + apiKey: "foo", + want: deserializeResp(endpointResponseExamples["/consumption_history/account"]["GET"].Content), + wantErr: false, + }, + { + name: "unhappy path", + args: args{ + from: time.Time{}, + to: time.Time{}, + granularity: "foo", + orgID: createPointer("foo"), + includeV1Metrics: createPointer(true), + }, + apiKey: "invalidApiKey", + want: ConsumptionHistoryPerAccountResponse{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + c, err := NewClient(Config{tt.apiKey, NewMockHTTPClient()}) + if err != nil { + panic(err) + } + got, err := c.GetConsumptionHistoryPerAccount(tt.args.from, tt.args.to, tt.args.granularity, tt.args.orgID, tt.args.includeV1Metrics) + if (err != nil) != tt.wantErr { + t.Errorf("GetConsumptionHistoryPerAccount() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetConsumptionHistoryPerAccount() got = %v, want %v", got, tt.want) + } + }, + ) + } +} + +func Test_client_GetConsumptionHistoryPerProject(t *testing.T) { + deserializeResp := func(s string) GetConsumptionHistoryPerProjectRespObj { + var v GetConsumptionHistoryPerProjectRespObj + if err := json.Unmarshal([]byte(s), &v); err != nil { + panic(err) + } + return v + } + type args struct { + cursor *string + limit *int + projectIDs []string + from time.Time + to time.Time + granularity ConsumptionHistoryGranularity + orgID *string + includeV1Metrics *bool + } + tests := []struct { + name string + args args + apiKey string + want GetConsumptionHistoryPerProjectRespObj + wantErr bool + }{ + { + name: "happy path", + args: args{ + cursor: createPointer("foo"), + limit: createPointer(1), + projectIDs: []string{"foo"}, + from: time.Time{}, + to: time.Time{}, + granularity: "foo", + orgID: createPointer("foo"), + includeV1Metrics: createPointer(true), + }, + apiKey: "foo", + want: deserializeResp(endpointResponseExamples["/consumption_history/projects"]["GET"].Content), + wantErr: false, + }, + { + name: "unhappy path", + args: args{ + cursor: createPointer("foo"), + limit: createPointer(1), + projectIDs: []string{"foo"}, + from: time.Time{}, + to: time.Time{}, + granularity: "foo", + orgID: createPointer("foo"), + includeV1Metrics: createPointer(true), + }, + apiKey: "invalidApiKey", + want: GetConsumptionHistoryPerProjectRespObj{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + c, err := NewClient(Config{tt.apiKey, NewMockHTTPClient()}) + if err != nil { + panic(err) + } + got, err := c.GetConsumptionHistoryPerProject(tt.args.cursor, tt.args.limit, tt.args.projectIDs, tt.args.from, tt.args.to, tt.args.granularity, tt.args.orgID, tt.args.includeV1Metrics) + if (err != nil) != tt.wantErr { + t.Errorf("GetConsumptionHistoryPerProject() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetConsumptionHistoryPerProject() got = %v, want %v", got, tt.want) + } + }, + ) + } +} + func Test_client_ListProjectsConsumption(t *testing.T) { deserializeResp := func(s string) ListProjectsConsumptionRespObj { var v ListProjectsConsumptionRespObj @@ -2832,6 +3111,7 @@ func Test_client_ListProjectsConsumption(t *testing.T) { limit *int from *time.Time to *time.Time + orgID *string } tests := []struct { name string @@ -2847,6 +3127,7 @@ func Test_client_ListProjectsConsumption(t *testing.T) { limit: createPointer(1), from: createPointer(time.Time{}), to: createPointer(time.Time{}), + orgID: createPointer("foo"), }, apiKey: "foo", want: deserializeResp(endpointResponseExamples["/consumption/projects"]["GET"].Content), @@ -2859,6 +3140,7 @@ func Test_client_ListProjectsConsumption(t *testing.T) { limit: createPointer(1), from: createPointer(time.Time{}), to: createPointer(time.Time{}), + orgID: createPointer("foo"), }, apiKey: "invalidApiKey", want: ListProjectsConsumptionRespObj{}, @@ -2872,7 +3154,7 @@ func Test_client_ListProjectsConsumption(t *testing.T) { if err != nil { panic(err) } - got, err := c.ListProjectsConsumption(tt.args.cursor, tt.args.limit, tt.args.from, tt.args.to) + got, err := c.ListProjectsConsumption(tt.args.cursor, tt.args.limit, tt.args.from, tt.args.to, tt.args.orgID) if (err != nil) != tt.wantErr { t.Errorf("ListProjectsConsumption() error = %v, wantErr %v", err, tt.wantErr) return @@ -2932,6 +3214,53 @@ func Test_client_GetCurrentUserInfo(t *testing.T) { } } +func Test_client_GetCurrentUserOrganizations(t *testing.T) { + deserializeResp := func(s string) OrganizationsResponse { + var v OrganizationsResponse + if err := json.Unmarshal([]byte(s), &v); err != nil { + panic(err) + } + return v + } + tests := []struct { + name string + apiKey string + want OrganizationsResponse + wantErr bool + }{ + { + name: "happy path", + apiKey: "foo", + want: deserializeResp(endpointResponseExamples["/users/me/organizations"]["GET"].Content), + wantErr: false, + }, + { + name: "unhappy path", + apiKey: "invalidApiKey", + want: OrganizationsResponse{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + c, err := NewClient(Config{tt.apiKey, NewMockHTTPClient()}) + if err != nil { + panic(err) + } + got, err := c.GetCurrentUserOrganizations() + if (err != nil) != tt.wantErr { + t.Errorf("GetCurrentUserOrganizations() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetCurrentUserOrganizations() got = %v, want %v", got, tt.want) + } + }, + ) + } +} + func TestTypes(t *testing.T) { // GIVEN // the types are defined correctly