4 KiB; the body is two strings patchTenantPinning sets the image and/or ducklake_version columns on a tenant's managed_warehouses row without touching the rest of the warehouse config. This replaces the operational shape of running `UPDATE duckgres_managed_warehouses SET image=..., ducklake_version=
(c *gin.Context)
| 921 | |
| 922 | // createOrgRequest is the POST /orgs body: the org config plus its REQUIRED |
| 923 | // first team (same contract as the provisioning API — an org cannot exist |
| 924 | // without a team). schema_name is optional; omitted derives the conventional |
| 925 | // "team_<id>". No billing semantics — the row is a plain team. |
| 926 | type createOrgRequest struct { |
| 927 | configstore.Org |
| 928 | TeamID int64 `json:"team_id"` |
| 929 | SchemaName string `json:"schema_name"` |
| 930 | } |
| 931 | |
| 932 | func (h *apiHandler) createOrg(c *gin.Context) { |
| 933 | var req createOrgRequest |
| 934 | if err := c.ShouldBindJSON(&req); err != nil { |
| 935 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) |
| 936 | return |
| 937 | } |
| 938 | org := req.Org |
| 939 | if err := validateOrgMutationPayload(&org); err != nil { |
| 940 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) |
| 941 | return |
| 942 | } |
| 943 | if org.Name == "" { |
| 944 | c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"}) |
| 945 | return |
| 946 | } |
| 947 | // database_name becomes the org's managed hostname label, so it must be a |
| 948 | // routable single DNS label at birth (same rule as the provisioning API). |
| 949 | // Trim the operator's value first so " acme" fails with the real problem |
| 950 | // instead of storing leading whitespace. |
| 951 | org.DatabaseName = strings.TrimSpace(org.DatabaseName) |
| 952 | if err := configstore.ValidateDatabaseName(org.DatabaseName); err != nil { |
| 953 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) |
| 954 | return |
| 955 | } |
| 956 | if req.TeamID <= 0 { |
| 957 | c.JSON(http.StatusBadRequest, gin.H{"error": "team_id is required (a positive PostHog team id — every org must have at least one team)"}) |
| 958 | return |
| 959 | } |
| 960 | schemaName := req.SchemaName |
| 961 | if schemaName == "" { |
| 962 | schemaName = fmt.Sprintf("team_%d", req.TeamID) |
| 963 | } |
| 964 | if err := configstore.ValidateOrgTeamSchemaName(schemaName); err != nil { |
| 965 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) |
| 966 | return |
| 967 | } |
| 968 | // Normalize empty hostname_alias to NULL on insert so the unique index |
| 969 | // doesn't reject a second org with an explicit empty string. Centralizing |
| 970 | // the rule at the handler layer keeps any future store impl from having |
| 971 | // to repeat it. |
| 972 | if org.HostnameAlias != nil && *org.HostnameAlias == "" { |
| 973 | org.HostnameAlias = nil |
| 974 | } |
| 975 | // POST /orgs has no :id param, so the audit org column is blank — record the |
| 976 | // created org's name here instead. |
| 977 | setAuditDetail(c, fmt.Sprintf("created org %s (first team %d, schema %s)", org.Name, req.TeamID, schemaName)) |
| 978 | if err := h.store.CreateOrg(&org, req.TeamID, schemaName); err != nil { |
| 979 | c.JSON(http.StatusConflict, gin.H{"error": err.Error()}) |
| 980 | return |
nothing calls this directly
no test coverage detected