diff --git a/api/compose/spec.json b/api/compose/spec.json index 6729ef0d0..18b3166bb 100644 --- a/api/compose/spec.json +++ b/api/compose/spec.json @@ -667,6 +667,68 @@ ] } }, + { + "name": "importInit", + "path": "/import", + "method": "POST", + "title": "Initiate record import session", + "parameters": { + "post": [ + { + "name": "upload", + "type": "*multipart.FileHeader", + "required": true, + "title": "File import" + } + ] + } + }, + { + "name": "importRun", + "path": "/import/{sessionID}", + "method": "PATCH", + "title": "Run record import", + "parameters": { + "path": [ + { + "name": "sessionID", + "type": "uint64", + "required": true, + "title": "Import session" + } + ], + "post": [ + { + "name": "fields", + "type": "json.RawMessage", + "required": true, + "title": "Fields defined by import file" + }, + { + "name": "onError", + "type": "string", + "required": true, + "title": "What happens if record fails to import" + } + ] + } + }, + { + "name": "importProgress", + "path": "/import/{sessionID}", + "method": "GET", + "title": "Get import progress", + "parameters": { + "path": [ + { + "name": "sessionID", + "type": "uint64", + "required": true, + "title": "Import session" + } + ] + } + }, { "name": "export", "path": "/export{filename}.{ext}", diff --git a/api/compose/spec/record.json b/api/compose/spec/record.json index f6aacf563..9b26fd875 100644 --- a/api/compose/spec/record.json +++ b/api/compose/spec/record.json @@ -91,6 +91,68 @@ ] } }, + { + "Name": "importInit", + "Method": "POST", + "Title": "Initiate record import session", + "Path": "/import", + "Parameters": { + "post": [ + { + "name": "upload", + "required": true, + "title": "File import", + "type": "*multipart.FileHeader" + } + ] + } + }, + { + "Name": "importRun", + "Method": "PATCH", + "Title": "Run record import", + "Path": "/import/{sessionID}", + "Parameters": { + "path": [ + { + "name": "sessionID", + "required": true, + "title": "Import session", + "type": "uint64" + } + ], + "post": [ + { + "name": "fields", + "required": true, + "title": "Fields defined by import file", + "type": "json.RawMessage" + }, + { + "name": "onError", + "required": true, + "title": "What happens if record fails to import", + "type": "string" + } + ] + } + }, + { + "Name": "importProgress", + "Method": "GET", + "Title": "Get import progress", + "Path": "/import/{sessionID}", + "Parameters": { + "path": [ + { + "name": "sessionID", + "required": true, + "title": "Import session", + "type": "uint64" + } + ] + } + }, { "Name": "export", "Method": "GET", diff --git a/compose/rest/handlers/record.go b/compose/rest/handlers/record.go index 46a2a6e13..495a337f0 100644 --- a/compose/rest/handlers/record.go +++ b/compose/rest/handlers/record.go @@ -31,6 +31,9 @@ import ( type RecordAPI interface { Report(context.Context, *request.RecordReport) (interface{}, error) List(context.Context, *request.RecordList) (interface{}, error) + ImportInit(context.Context, *request.RecordImportInit) (interface{}, error) + ImportRun(context.Context, *request.RecordImportRun) (interface{}, error) + ImportProgress(context.Context, *request.RecordImportProgress) (interface{}, error) Export(context.Context, *request.RecordExport) (interface{}, error) Create(context.Context, *request.RecordCreate) (interface{}, error) Read(context.Context, *request.RecordRead) (interface{}, error) @@ -41,14 +44,17 @@ type RecordAPI interface { // HTTP API interface type Record struct { - Report func(http.ResponseWriter, *http.Request) - List func(http.ResponseWriter, *http.Request) - Export func(http.ResponseWriter, *http.Request) - Create func(http.ResponseWriter, *http.Request) - Read func(http.ResponseWriter, *http.Request) - Update func(http.ResponseWriter, *http.Request) - Delete func(http.ResponseWriter, *http.Request) - Upload func(http.ResponseWriter, *http.Request) + Report func(http.ResponseWriter, *http.Request) + List func(http.ResponseWriter, *http.Request) + ImportInit func(http.ResponseWriter, *http.Request) + ImportRun func(http.ResponseWriter, *http.Request) + ImportProgress func(http.ResponseWriter, *http.Request) + Export func(http.ResponseWriter, *http.Request) + Create func(http.ResponseWriter, *http.Request) + Read func(http.ResponseWriter, *http.Request) + Update func(http.ResponseWriter, *http.Request) + Delete func(http.ResponseWriter, *http.Request) + Upload func(http.ResponseWriter, *http.Request) } func NewRecord(h RecordAPI) *Record { @@ -93,6 +99,66 @@ func NewRecord(h RecordAPI) *Record { resputil.JSON(w, value) } }, + ImportInit: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewRecordImportInit() + if err := params.Fill(r); err != nil { + logger.LogParamError("Record.ImportInit", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.ImportInit(r.Context(), params) + if err != nil { + logger.LogControllerError("Record.ImportInit", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("Record.ImportInit", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + ImportRun: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewRecordImportRun() + if err := params.Fill(r); err != nil { + logger.LogParamError("Record.ImportRun", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.ImportRun(r.Context(), params) + if err != nil { + logger.LogControllerError("Record.ImportRun", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("Record.ImportRun", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + ImportProgress: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewRecordImportProgress() + if err := params.Fill(r); err != nil { + logger.LogParamError("Record.ImportProgress", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.ImportProgress(r.Context(), params) + if err != nil { + logger.LogControllerError("Record.ImportProgress", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("Record.ImportProgress", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, Export: func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() params := request.NewRecordExport() @@ -221,6 +287,9 @@ func (h Record) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http r.Use(middlewares...) r.Get("/namespace/{namespaceID}/module/{moduleID}/record/report", h.Report) r.Get("/namespace/{namespaceID}/module/{moduleID}/record/", h.List) + r.Post("/namespace/{namespaceID}/module/{moduleID}/record/import", h.ImportInit) + r.Patch("/namespace/{namespaceID}/module/{moduleID}/record/import/{sessionID}", h.ImportRun) + r.Get("/namespace/{namespaceID}/module/{moduleID}/record/import/{sessionID}", h.ImportProgress) r.Get("/namespace/{namespaceID}/module/{moduleID}/record/export{filename}.{ext}", h.Export) r.Post("/namespace/{namespaceID}/module/{moduleID}/record/", h.Create) r.Get("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}", h.Read) diff --git a/compose/rest/request/record.go b/compose/rest/request/record.go index 8cc5b9d77..bc4bf8a5a 100644 --- a/compose/rest/request/record.go +++ b/compose/rest/request/record.go @@ -175,6 +175,192 @@ func (r *RecordList) Fill(req *http.Request) (err error) { var _ RequestFiller = NewRecordList() +// Record importInit request parameters +type RecordImportInit struct { + Upload *multipart.FileHeader + NamespaceID uint64 `json:",string"` + ModuleID uint64 `json:",string"` +} + +func NewRecordImportInit() *RecordImportInit { + return &RecordImportInit{} +} + +func (r RecordImportInit) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["upload.size"] = r.Upload.Size + out["upload.filename"] = r.Upload.Filename + + out["namespaceID"] = r.NamespaceID + out["moduleID"] = r.ModuleID + + return out +} + +func (r *RecordImportInit) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseMultipartForm(32 << 20); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + if _, r.Upload, err = req.FormFile("upload"); err != nil { + return errors.Wrap(err, "error procesing uploaded file") + } + + r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID")) + r.ModuleID = parseUInt64(chi.URLParam(req, "moduleID")) + + return err +} + +var _ RequestFiller = NewRecordImportInit() + +// Record importRun request parameters +type RecordImportRun struct { + SessionID uint64 `json:",string"` + NamespaceID uint64 `json:",string"` + ModuleID uint64 `json:",string"` + Fields json.RawMessage + OnError string +} + +func NewRecordImportRun() *RecordImportRun { + return &RecordImportRun{} +} + +func (r RecordImportRun) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["sessionID"] = r.SessionID + out["namespaceID"] = r.NamespaceID + out["moduleID"] = r.ModuleID + out["fields"] = r.Fields + out["onError"] = r.OnError + + return out +} + +func (r *RecordImportRun) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + r.SessionID = parseUInt64(chi.URLParam(req, "sessionID")) + r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID")) + r.ModuleID = parseUInt64(chi.URLParam(req, "moduleID")) + if val, ok := post["fields"]; ok { + r.Fields = json.RawMessage(val) + } + if val, ok := post["onError"]; ok { + r.OnError = val + } + + return err +} + +var _ RequestFiller = NewRecordImportRun() + +// Record importProgress request parameters +type RecordImportProgress struct { + SessionID uint64 `json:",string"` + NamespaceID uint64 `json:",string"` + ModuleID uint64 `json:",string"` +} + +func NewRecordImportProgress() *RecordImportProgress { + return &RecordImportProgress{} +} + +func (r RecordImportProgress) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["sessionID"] = r.SessionID + out["namespaceID"] = r.NamespaceID + out["moduleID"] = r.ModuleID + + return out +} + +func (r *RecordImportProgress) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + r.SessionID = parseUInt64(chi.URLParam(req, "sessionID")) + r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID")) + r.ModuleID = parseUInt64(chi.URLParam(req, "moduleID")) + + return err +} + +var _ RequestFiller = NewRecordImportProgress() + // Record export request parameters type RecordExport struct { Filter string diff --git a/docs/compose/README.md b/docs/compose/README.md index 549b87d90..2400c9e89 100644 --- a/docs/compose/README.md +++ b/docs/compose/README.md @@ -956,6 +956,9 @@ Compose records | ------ | -------- | ------- | | `GET` | `/namespace/{namespaceID}/module/{moduleID}/record/report` | Generates report from module records | | `GET` | `/namespace/{namespaceID}/module/{moduleID}/record/` | List/read records from module section | +| `POST` | `/namespace/{namespaceID}/module/{moduleID}/record/import` | Initiate record import session | +| `PATCH` | `/namespace/{namespaceID}/module/{moduleID}/record/import/{sessionID}` | Run record import | +| `GET` | `/namespace/{namespaceID}/module/{moduleID}/record/import/{sessionID}` | Get import progress | | `GET` | `/namespace/{namespaceID}/module/{moduleID}/record/export{filename}.{ext}` | Exports records that match | | `POST` | `/namespace/{namespaceID}/module/{moduleID}/record/` | Create record in module section | | `GET` | `/namespace/{namespaceID}/module/{moduleID}/record/{recordID}` | Read records by ID from module section | @@ -1000,6 +1003,56 @@ Compose records | namespaceID | uint64 | PATH | Namespace ID | N/A | YES | | moduleID | uint64 | PATH | Module ID | N/A | YES | +## Initiate record import session + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/namespace/{namespaceID}/module/{moduleID}/record/import` | HTTP/S | POST | | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| upload | *multipart.FileHeader | POST | File import | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | +| moduleID | uint64 | PATH | Module ID | N/A | YES | + +## Run record import + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/namespace/{namespaceID}/module/{moduleID}/record/import/{sessionID}` | HTTP/S | PATCH | | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| sessionID | uint64 | PATH | Import session | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | +| moduleID | uint64 | PATH | Module ID | N/A | YES | +| fields | json.RawMessage | POST | Fields defined by import file | N/A | YES | +| onError | string | POST | What happens if record fails to import | N/A | YES | + +## Get import progress + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/namespace/{namespaceID}/module/{moduleID}/record/import/{sessionID}` | HTTP/S | GET | | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| sessionID | uint64 | PATH | Import session | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | +| moduleID | uint64 | PATH | Module ID | N/A | YES | + ## Exports records that match #### Method