diff --git a/internal/dev_server/api/api.yaml b/internal/dev_server/api/api.yaml index c2d9dcb8..cc62585a 100644 --- a/internal/dev_server/api/api.yaml +++ b/internal/dev_server/api/api.yaml @@ -113,6 +113,28 @@ paths: $ref: "#/components/responses/ErrorResponse" 409: $ref: "#/components/responses/ErrorResponse" + /projects/{projectKey}/import: + post: + summary: Import a project from exported JSON data + description: | + Import a project using the JSON format from get-project with expand options. + Accepts the same format that is exported by the get-project endpoint. + operationId: postImportProject + parameters: + - $ref: "#/components/parameters/projectKey" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Project" + responses: + 201: + $ref: "#/components/responses/Project" + 400: + $ref: "#/components/responses/ErrorResponse" + 409: + $ref: "#/components/responses/ErrorResponse" /projects/{projectKey}/overrides: delete: summary: remove all overrides for the given project diff --git a/internal/dev_server/api/post_import_project.go b/internal/dev_server/api/post_import_project.go new file mode 100644 index 00000000..37470bc5 --- /dev/null +++ b/internal/dev_server/api/post_import_project.go @@ -0,0 +1,82 @@ +package api + +import ( + "context" + + "github.com/pkg/errors" + + "github.com/launchdarkly/ldcli/internal/dev_server/model" +) + +func (s server) PostImportProject(ctx context.Context, request PostImportProjectRequestObject) (PostImportProjectResponseObject, error) { + // Validate required fields + if request.Body.SourceEnvironmentKey == "" { + return PostImportProject400JSONResponse{ + ErrorResponseJSONResponse{ + Code: "invalid_request", + Message: "sourceEnvironmentKey is required", + }, + }, nil + } + + if request.Body.FlagsState == nil || len(*request.Body.FlagsState) == 0 { + return PostImportProject400JSONResponse{ + ErrorResponseJSONResponse{ + Code: "invalid_request", + Message: "flagsState is required", + }, + }, nil + } + + // Build import data from request + importData := model.ImportData{ + Context: request.Body.Context, + SourceEnvironmentKey: request.Body.SourceEnvironmentKey, + FlagsState: *request.Body.FlagsState, + Overrides: request.Body.Overrides, + } + + // Convert availableVariations if present + if request.Body.AvailableVariations != nil { + variations := make(map[string][]model.ImportVariation) + for flagKey, vars := range *request.Body.AvailableVariations { + for _, v := range vars { + variations[flagKey] = append(variations[flagKey], model.ImportVariation{ + Id: v.Id, + Name: v.Name, + Description: v.Description, + Value: v.Value, + }) + } + } + importData.AvailableVariations = &variations + } + + // Import the project + err := model.ImportProject(ctx, request.ProjectKey, importData) + switch { + case errors.As(err, &model.ErrAlreadyExists{}): + return PostImportProject409JSONResponse{ + Code: "conflict", + Message: err.Error(), + }, nil + case err != nil: + return nil, err + } + + // Fetch the imported project to return + store := model.StoreFromContext(ctx) + project, err := store.GetDevProject(ctx, request.ProjectKey) + if err != nil { + return nil, err + } + + response := ProjectJSONResponse{ + LastSyncedFromSource: project.LastSyncTime.Unix(), + Context: project.Context, + SourceEnvironmentKey: project.SourceEnvironmentKey, + FlagsState: &project.AllFlagsState, + } + + return PostImportProject201JSONResponse{response}, nil +} diff --git a/internal/dev_server/api/server.gen.go b/internal/dev_server/api/server.gen.go index 0f724947..02c26907 100644 --- a/internal/dev_server/api/server.gen.go +++ b/internal/dev_server/api/server.gen.go @@ -239,6 +239,9 @@ type PatchProjectJSONRequestBody PatchProjectJSONBody // PostAddProjectJSONRequestBody defines body for PostAddProject for application/json ContentType. type PostAddProjectJSONRequestBody PostAddProjectJSONBody +// PostImportProjectJSONRequestBody defines body for PostImportProject for application/json ContentType. +type PostImportProjectJSONRequestBody = Project + // PutOverrideFlagJSONRequestBody defines body for PutOverrideFlag for application/json ContentType. type PutOverrideFlagJSONRequestBody = FlagValue @@ -277,6 +280,9 @@ type ServerInterface interface { // list all environments for the given project // (GET /projects/{projectKey}/environments) GetEnvironments(w http.ResponseWriter, r *http.Request, projectKey ProjectKey, params GetEnvironmentsParams) + // Import a project from exported JSON data + // (POST /projects/{projectKey}/import) + PostImportProject(w http.ResponseWriter, r *http.Request, projectKey ProjectKey) // remove all overrides for the given project // (DELETE /projects/{projectKey}/overrides) DeleteOverrides(w http.ResponseWriter, r *http.Request, projectKey ProjectKey) @@ -628,6 +634,31 @@ func (siw *ServerInterfaceWrapper) GetEnvironments(w http.ResponseWriter, r *htt handler.ServeHTTP(w, r) } +// PostImportProject operation middleware +func (siw *ServerInterfaceWrapper) PostImportProject(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "projectKey" ------------- + var projectKey ProjectKey + + err = runtime.BindStyledParameterWithOptions("simple", "projectKey", mux.Vars(r)["projectKey"], &projectKey, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "projectKey", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.PostImportProject(w, r, projectKey) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // DeleteOverrides operation middleware func (siw *ServerInterfaceWrapper) DeleteOverrides(w http.ResponseWriter, r *http.Request) { @@ -856,6 +887,8 @@ func HandlerWithOptions(si ServerInterface, options GorillaServerOptions) http.H r.HandleFunc(options.BaseURL+"/projects/{projectKey}/environments", wrapper.GetEnvironments).Methods("GET") + r.HandleFunc(options.BaseURL+"/projects/{projectKey}/import", wrapper.PostImportProject).Methods("POST") + r.HandleFunc(options.BaseURL+"/projects/{projectKey}/overrides", wrapper.DeleteOverrides).Methods("DELETE") r.HandleFunc(options.BaseURL+"/projects/{projectKey}/overrides/{flagKey}", wrapper.DeleteFlagOverride).Methods("DELETE") @@ -1202,6 +1235,48 @@ func (response GetEnvironments404JSONResponse) VisitGetEnvironmentsResponse(w ht return json.NewEncoder(w).Encode(response) } +type PostImportProjectRequestObject struct { + ProjectKey ProjectKey `json:"projectKey"` + Body *PostImportProjectJSONRequestBody +} + +type PostImportProjectResponseObject interface { + VisitPostImportProjectResponse(w http.ResponseWriter) error +} + +type PostImportProject201JSONResponse struct{ ProjectJSONResponse } + +func (response PostImportProject201JSONResponse) VisitPostImportProjectResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(201) + + return json.NewEncoder(w).Encode(response) +} + +type PostImportProject400JSONResponse struct{ ErrorResponseJSONResponse } + +func (response PostImportProject400JSONResponse) VisitPostImportProjectResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type PostImportProject409JSONResponse struct { + // Code specific error code encountered + Code string `json:"code"` + + // Message description of the error + Message string `json:"message"` +} + +func (response PostImportProject409JSONResponse) VisitPostImportProjectResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(409) + + return json.NewEncoder(w).Encode(response) +} + type DeleteOverridesRequestObject struct { ProjectKey ProjectKey `json:"projectKey"` } @@ -1315,6 +1390,9 @@ type StrictServerInterface interface { // list all environments for the given project // (GET /projects/{projectKey}/environments) GetEnvironments(ctx context.Context, request GetEnvironmentsRequestObject) (GetEnvironmentsResponseObject, error) + // Import a project from exported JSON data + // (POST /projects/{projectKey}/import) + PostImportProject(ctx context.Context, request PostImportProjectRequestObject) (PostImportProjectResponseObject, error) // remove all overrides for the given project // (DELETE /projects/{projectKey}/overrides) DeleteOverrides(ctx context.Context, request DeleteOverridesRequestObject) (DeleteOverridesResponseObject, error) @@ -1656,6 +1734,39 @@ func (sh *strictHandler) GetEnvironments(w http.ResponseWriter, r *http.Request, } } +// PostImportProject operation middleware +func (sh *strictHandler) PostImportProject(w http.ResponseWriter, r *http.Request, projectKey ProjectKey) { + var request PostImportProjectRequestObject + + request.ProjectKey = projectKey + + var body PostImportProjectJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.PostImportProject(ctx, request.(PostImportProjectRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "PostImportProject") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(PostImportProjectResponseObject); ok { + if err := validResponse.VisitPostImportProjectResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // DeleteOverrides operation middleware func (sh *strictHandler) DeleteOverrides(w http.ResponseWriter, r *http.Request, projectKey ProjectKey) { var request DeleteOverridesRequestObject