diff --git a/README.md b/README.md index 82d5521..f37fc34 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ to access [kintone][] with its official REST API ([en][APIen], [ja][APIja]). ## Version -0.2.0 +0.3.0 ## License diff --git a/app.go b/app.go index aace325..60007a9 100644 --- a/app.go +++ b/app.go @@ -24,8 +24,8 @@ import ( ) const ( - NAME = "kintone-go-SDK" - VERSION = "0.2.0" + NAME = "go-kintone" + VERSION = "0.3.0" DEFAULT_TIMEOUT = time.Second * 600 // Default value for App.Timeout ) @@ -124,7 +124,7 @@ type App struct { basicAuth bool // true to use Basic Authentication. basicAuthUser string // User name for Basic Authentication. basicAuthPassword string // Password for Basic Authentication. - userAgentHeader string // User-agent request header string + extUserAgent string // User-agent request header string } // SetBasicAuth enables use of HTTP basic authentication for access @@ -152,12 +152,71 @@ func (app *App) GetBasicAuthPassword() string { // SetUserAgentHeader set custom user-agent header for http request func (app *App) SetUserAgentHeader(userAgentHeader string) { - app.userAgentHeader = userAgentHeader + app.extUserAgent = userAgentHeader } // GetUserAgentHeader get user-agent header string func (app *App) GetUserAgentHeader() string { - return app.userAgentHeader + userAgent := NAME + "/" + VERSION + if len(app.extUserAgent) > 0 { + return userAgent + " " + app.extUserAgent + } + return userAgent +} + +func (app *App) createUrl(api string, query string) url.URL { + path := fmt.Sprintf("/k/v1/%s.json", api) + if app.GuestSpaceId > 0 { + path = fmt.Sprintf("/k/guest/%d/v1/%s.json", app.GuestSpaceId, api) + } + + resultUrl := url.URL{ + Scheme: "https", + Host: app.Domain, + Path: path, + } + + if len(query) > 0 { + resultUrl.RawQuery = query + } + return resultUrl +} +func (app *App) setAuth(request *http.Request) { + if app.basicAuth { + request.SetBasicAuth(app.basicAuthUser, app.basicAuthPassword) + } + + if len(app.ApiToken) > 0 { + request.Header.Set("X-Cybozu-API-Token", app.ApiToken) + } + + if len(app.User) > 0 && len(app.Password) > 0 { + request.Header.Set("X-Cybozu-Authorization", base64.StdEncoding.EncodeToString( + []byte(app.User+":"+app.Password))) + } +} + +//NewRequest create a request connect to kintone api. +func (app *App) NewRequest(method, url string, body io.Reader) (*http.Request, error) { + bodyData := io.Reader(nil) + if body != nil { + bodyData = body + } + + request, err := http.NewRequest(method, url, bodyData) + if err != nil { + return nil, err + } + + request.Header.Set("User-Agent", app.GetUserAgentHeader()) + + if method != "GET" { + request.Header.Set("Content-Type", "application/json") + } + + app.setAuth(request) + + return request, nil } func (app *App) newRequest(method, api string, body io.Reader) (*http.Request, error) { @@ -191,12 +250,8 @@ func (app *App) newRequest(method, api string, body io.Reader) (*http.Request, e req.Header.Set("X-Cybozu-API-Token", app.ApiToken) } req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", app.GetUserAgentHeader()) - if len(app.GetUserAgentHeader()) != 0 { - req.Header.Set("User-Agent", app.userAgentHeader) - } else { - req.Header.Set("User-Agent", NAME+"/"+VERSION) - } return req, nil } @@ -1000,3 +1055,82 @@ func (app *App) Fields() (map[string]*FieldInfo, error) { } return ret, nil } + +//CreateCursor return the meta data of the Cursor in this application +func (app *App) CreateCursor(fields []string, query string, size uint64) (*Cursor, error) { + type cursor struct { + App uint64 `json:"app"` + Fields []string `json:"fields"` + Size uint64 `json:"size"` + Query string `json:"query"` + } + data := cursor{App: app.AppId, Fields: fields, Size: size, Query: query} + jsonData, _ := json.Marshal(data) + url := app.createUrl("records/cursor", "") + request, err := app.NewRequest("POST", url.String(), bytes.NewBuffer(jsonData)) + if err != nil { + return nil, err + } + response, err := app.do(request) + if err != nil { + return nil, err + } + body, err := parseResponse(response) + if err != nil { + return nil, err + } + result, err := decodeCursor(body) + return result, nil +} + +// DeleteCursor - Delete cursor by id +func (app *App) DeleteCursor(id string) error { + type requestBody struct { + Id string `json:"id"` + } + data, err := json.Marshal(requestBody{Id: id}) + if err != nil { + return err + } + + url := app.createUrl("records/cursor", "") + request, err := app.NewRequest("DELETE", url.String(), bytes.NewBuffer(data)) + if err != nil { + return err + } + + response, err := app.do(request) + if err != nil { + return err + } + + _, err = parseResponse(response) + if err != nil { + return err + } + + return nil +} + +//Using Cursor Id to get all records +//GetRecordsByCursor return the meta data of the Record in this application +func (app *App) GetRecordsByCursor(id string) (*GetRecordsCursorResponse, error) { + url := app.createUrl("records/cursor", "id="+id) + request, err := app.NewRequest("GET", url.String(), nil) + if err != nil { + return nil, err + } + response, err := app.do(request) + if err != nil { + return nil, err + } + data, err := parseResponse(response) + if err != nil { + return nil, err + } + recordsCursorResponse, err := DecodeGetRecordsCursorResponse(data) + if err != nil { + return nil, err + } + return recordsCursorResponse, nil +} diff --git a/app_test.go b/app_test.go index d9e8f7b..c1fe851 100644 --- a/app_test.go +++ b/app_test.go @@ -5,94 +5,231 @@ package kintone import ( - "bytes" + "crypto/tls" + "encoding/base64" + "fmt" + "io" "io/ioutil" + "net" + "net/http" + "net/http/httptest" "os" "strings" "testing" "time" ) -func newApp(appID uint64) *App { - return &App{ - Domain: os.Getenv("KINTONE_DOMAIN"), - User: os.Getenv("KINTONE_USER"), - Password: os.Getenv("KINTONE_PASSWORD"), - AppId: appID, +const ( + KINTONE_DOMAIN = "localhost:8088" + KINTONE_USERNAME = "test" + KINTONE_PASSWORD = "test" + KINTONE_APP_ID = 1 + KINTONE_API_TOKEN = "1e42da75-8432-4adb-9a2b-dbb6e7cb3c6b" + KINTONE_GUEST_SPACE_ID = 1 + AUTH_HEADER_TOKEN = "X-Cybozu-API-Token" + AUTH_HEADER_PASSWORD = "X-Cybozu-Authorization" + AUTH_HEADER_BASIC = "Authorization" + CONTENT_TYPE = "Content-Type" + APPLICATION_JSON = "application/json" + BASIC_AUTH = true + BASIC_AUTH_USER = "basic" + BASIC_AUTH_PASSWORD = "basic" +) + +func createServerTest(mux *http.ServeMux) (*httptest.Server, error) { + ts := httptest.NewUnstartedServer(mux) + listen, err := net.Listen("tcp", KINTONE_DOMAIN) + + if err != nil { + return nil, err } + + ts.Listener.Close() + ts.Listener = listen + ts.StartTLS() + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + return ts, nil } -func newAppWithApiToken(appId uint64) *App { - return &App{ - Domain: os.Getenv("KINTONE_DOMAIN"), - ApiToken: os.Getenv("KINTONE_API_TOKEN"), - AppId: appId, +func createServerMux() *http.ServeMux { + mux := http.NewServeMux() + mux.HandleFunc("/k/v1/record.json", handleResponseGetRecord) + mux.HandleFunc("/k/v1/records.json", handleResponseGetRecords) + mux.HandleFunc("/k/v1/record/comments.json", handleResponseGetRecordsComments) + mux.HandleFunc("/k/v1/file.json", handleResponseUploadFile) + mux.HandleFunc("/k/v1/record/comment.json", handleResponseRecordComments) + mux.HandleFunc("/k/v1/records/cursor.json", handleResponseRecordsCursor) + mux.HandleFunc("/k/v1/app/status.json", handleResponseProcess) + mux.HandleFunc("/k/v1/form.json", handleResponseForm) + mux.HandleFunc("/k/guest/1/v1/form.json", handleResponseForm) + return mux +} + +// header check +func checkAuth(response http.ResponseWriter, request *http.Request) { + authPassword := request.Header.Get(AUTH_HEADER_PASSWORD) + authToken := request.Header.Get(AUTH_HEADER_TOKEN) + authBasic := request.Header.Get(AUTH_HEADER_BASIC) + + userAndPass := base64.StdEncoding.EncodeToString( + []byte(KINTONE_USERNAME + ":" + KINTONE_USERNAME)) + + userAndPassBasic := "Basic " + base64.StdEncoding.EncodeToString( + []byte(BASIC_AUTH_USER+":"+BASIC_AUTH_PASSWORD)) + + if authToken == "" && authPassword == "" && authBasic == "" { + http.Error(response, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + } + if BASIC_AUTH && authBasic != "" && authBasic != userAndPassBasic { + http.Error(response, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + } + if authToken != "" && authToken != KINTONE_API_TOKEN { + http.Error(response, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + } + if authPassword != "" && authPassword != userAndPass { + http.Error(response, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) } } -func newAppInGuestSpace(appId uint64, guestSpaceId uint64) *App { - return &App{ - Domain: os.Getenv("KINTONE_DOMAIN"), - User: os.Getenv("KINTONE_USER"), - Password: os.Getenv("KINTONE_PASSWORD"), - AppId: appId, - GuestSpaceId: guestSpaceId, +func checkContentType(response http.ResponseWriter, request *http.Request) { + contentType := request.Header.Get(CONTENT_TYPE) + if contentType != APPLICATION_JSON { + http.Error(response, http.StatusText(http.StatusNoContent), http.StatusNoContent) } } -func TestGetRecord(t *testing.T) { - a := newApp(4799) - if len(a.Password) == 0 { - t.Skip() +// handler mux +func handleResponseProcess(response http.ResponseWriter, request *http.Request) { + checkAuth(response, request) + TestData := GetTestDataProcess() + fmt.Fprint(response, TestData.output) +} + +func handleResponseForm(response http.ResponseWriter, request *http.Request) { + checkAuth(response, request) + if request.Method == "GET" { + checkContentType(response, request) + testData := GetDataTestForm() + fmt.Fprint(response, testData.output) } +} - if rec, err := a.GetRecord(116); err != nil { - t.Error(err) - } else { - if rec.Id() != 116 { - t.Errorf("Unexpected Id: %d", rec.Id()) - } - for _, f := range rec.Fields { - if files, ok := f.(FileField); ok { - if len(files) == 0 { - continue - } - fd, err := a.Download(files[0].FileKey) - if err != nil { - t.Error(err) - } else { - data, _ := ioutil.ReadAll(fd.Reader) - t.Logf("%s %d bytes", fd.ContentType, len(data)) - } - } - } +func handleResponseRecordsCursor(response http.ResponseWriter, request *http.Request) { + checkAuth(response, request) + if request.Method == "GET" { + testData := GetDataTestGetRecordsByCursor() + fmt.Fprint(response, testData.output) + } else if request.Method == "DELETE" { + checkContentType(response, request) + testData := GetTestDataDeleteCursor() + fmt.Fprint(response, testData.output) + } else if request.Method == "POST" { + checkContentType(response, request) + testData := GetTestDataCreateCursor() + fmt.Fprint(response, testData.output) } +} - if recs, err := a.GetRecords(nil, "limit 3 offset 3"); err != nil { - t.Error(err) - } else { - if len(recs) > 3 { - t.Error("Too many records") - } +func handleResponseRecordComments(response http.ResponseWriter, request *http.Request) { + checkAuth(response, request) + checkContentType(response, request) + if request.Method == "POST" { + testData := GetTestDataAddRecordComment() + fmt.Fprint(response, testData.output) + } else if request.Method == "DELETE" { + testData := GetDataTestDeleteRecordComment() + fmt.Fprint(response, testData.output) } +} - if recs, err := a.GetAllRecords([]string{"レコード番号"}); err != nil { - t.Error(err) - } else { - t.Log(len(recs)) +func handleResponseUploadFile(response http.ResponseWriter, request *http.Request) { + checkAuth(response, request) + if request.Method == "POST" { + testData := GetDataTestUploadFile() + fmt.Fprint(response, testData.output) } } -func TestAddRecord(t *testing.T) { - a := newApp(9004) - if len(a.Password) == 0 { - t.Skip() +func handleResponseGetRecord(response http.ResponseWriter, request *http.Request) { + checkAuth(response, request) + checkContentType(response, request) + if request.Method == "GET" { + testData := GetTestDataGetRecord() + fmt.Fprint(response, testData.output) + } else if request.Method == "PUT" { + testData := GetTestDataUpdateRecordByKey() + fmt.Fprint(response, testData.output) + } else if request.Method == "POST" { + testData := GetTestDataAddRecord() + fmt.Fprint(response, testData.output) + } +} + +func handleResponseGetRecords(response http.ResponseWriter, request *http.Request) { + checkAuth(response, request) + checkContentType(response, request) + if request.Method == "GET" { + testData := GetTestDataGetRecords() + fmt.Fprint(response, testData.output) + } else if request.Method == "DELETE" { + testData := GetTestDataDeleteRecords() + fmt.Fprint(response, testData.output) + } else if request.Method == "POST" { + testData := GetTestDataAddRecords() + fmt.Fprint(response, testData.output) + } +} + +func handleResponseGetRecordsComments(response http.ResponseWriter, request *http.Request) { + checkAuth(response, request) + checkContentType(response, request) + testData := GetDataTestRecordComments() + fmt.Fprint(response, testData.output) +} + +func TestMain(m *testing.M) { + mux := createServerMux() + ts, err := createServerTest(mux) + if err != nil { + fmt.Println("createServerTest: ", err) + os.Exit(1) + } + code := m.Run() + ts.Close() + os.Exit(code) +} + +func newApp() *App { + return &App{ + Domain: KINTONE_DOMAIN, + User: KINTONE_USERNAME, + Password: KINTONE_PASSWORD, + AppId: KINTONE_APP_ID, + } +} +func newAppWithGuest() *App { + return &App{ + Domain: KINTONE_DOMAIN, + AppId: KINTONE_APP_ID, + User: KINTONE_USERNAME, + Password: KINTONE_PASSWORD, + GuestSpaceId: KINTONE_GUEST_SPACE_ID, + } +} +func newAppWithToken() *App { + return &App{ + AppId: KINTONE_APP_ID, + Domain: KINTONE_DOMAIN, + ApiToken: KINTONE_API_TOKEN, } +} + +func TestAddRecord(t *testing.T) { + testData := GetDataTestAddRecord() + app := newApp() - fileKey, err := a.Upload("ほげ春巻.txta", "text/html", - bytes.NewReader([]byte(`abc -hoge -`))) + fileKey, err := app.Upload(testData.input[0].(string), testData.input[2].(string), + testData.input[1].(io.Reader)) if err != nil { t.Error("Upload failed", err) } @@ -103,11 +240,10 @@ func TestAddRecord(t *testing.T) { {FileKey: fileKey}, }, }) - _, err = a.AddRecord(rec) + _, err = app.AddRecord(rec) if err != nil { t.Error("AddRecord failed", rec) } - recs := []*Record{ NewRecord(map[string]interface{}{ "title": SingleLineTextField("multi add 1"), @@ -116,68 +252,137 @@ func TestAddRecord(t *testing.T) { "title": SingleLineTextField("multi add 2"), }), } - ids, err := a.AddRecords(recs) + ids, err := app.AddRecords(recs) if err != nil { t.Error("AddRecords failed", recs) } else { t.Log(ids) } } +func TestGetRecord(t *testing.T) { + testData := GetTestDataGetRecord() + testDataRecords := GetTestDataGetRecords() + app := newApp() + if rec, err := app.GetRecord(uint64(testData.input[0].(int))); err != nil { + t.Error(err) + } else { + if rec.Id() != 1 { + t.Errorf("Unexpected Id: %d", rec.Id()) + } + for _, f := range rec.Fields { + if files, ok := f.(FileField); ok { + if len(files) == 0 { + continue + } + fd, err := app.Download(files[0].FileKey) + if err != nil { + t.Error(err) + } else { + data, _ := ioutil.ReadAll(fd.Reader) + t.Logf("%s %d bytes", fd.ContentType, len(data)) + } + } + } + } -func TestUpdateRecord(t *testing.T) { - a := newApp(9004) - if len(a.Password) == 0 { - t.Skip() + if recs, err := app.GetRecords(testDataRecords.input[0].([]string), testDataRecords.input[1].(string)); err != nil { + t.Error(err) + } else { + if len(recs) > 3 { + t.Error("Too many records") + } } - rec, err := a.GetRecord(4) + if recs, err := app.GetAllRecords(testDataRecords.input[0].([]string)); err != nil { + t.Error(err) + } else { + t.Log(len(recs)) + } + +} +func TestUpdateRecord(t *testing.T) { + testData := GetTestDataGetRecord() + testDataRecords := GetTestDataGetRecords() + testDataRecordByKey := GetTestDataUpdateRecordByKey() + + app := newApp() + + rec, err := app.GetRecord(uint64(testData.input[0].(int))) if err != nil { t.Fatal(err) } rec.Fields["title"] = SingleLineTextField("new title") - if err := a.UpdateRecord(rec, true); err != nil { + if err := app.UpdateRecord(rec, testData.input[1].(bool)); err != nil { t.Error("UpdateRecord failed", err) } - if err := a.UpdateRecordByKey(rec, true, "key"); err != nil { + rec.Fields[testDataRecordByKey.input[1].(string)] = SingleLineTextField(` { + "field": "unique_key", + "value": "unique_code" + }`) + if err := app.UpdateRecordByKey(rec, testData.input[1].(bool), testDataRecordByKey.input[1].(string)); err != nil { t.Error("UpdateRecordByKey failed", err) } - - recs, err := a.GetRecords(nil, "limit 3") + recs, err := app.GetRecords(testDataRecords.input[0].([]string), testDataRecords.input[1].(string)) if err != nil { t.Fatal(err) } + for _, rec := range recs { rec.Fields["title"] = SingleLineTextField(time.Now().String()) + rec.Fields["key"] = SingleLineTextField(` { + "field": "unique_key", + "value": "unique_code" + }`) } - if err := a.UpdateRecords(recs, true); err != nil { + if err := app.UpdateRecords(recs, testData.input[1].(bool)); err != nil { t.Error("UpdateRecords failed", err) } - if err := a.UpdateRecordsByKey(recs, true, "key"); err != nil { + if err := app.UpdateRecordsByKey(recs, testDataRecordByKey.input[2].(bool), testDataRecordByKey.input[1].(string)); err != nil { t.Error("UpdateRecordsByKey failed", err) } } func TestDeleteRecord(t *testing.T) { - a := newApp(9004) - if len(a.Password) == 0 { - t.Skip() + testData := GetTestDataDeleteRecords() + app := newApp() + if err := app.DeleteRecords(testData.input[0].([]uint64)); err != nil { + t.Error("DeleteRecords failed", err) } +} - ids := []uint64{6, 7} - if err := a.DeleteRecords(ids); err != nil { - t.Error("DeleteRecords failed", err) +func TestGetRecordsByCursor(t *testing.T) { + testData := GetDataTestGetRecordsByCursor() + app := newApp() + _, err := app.GetRecordsByCursor(testData.input[0].(string)) + if err != nil { + t.Errorf("TestGetCursor is failed: %v", err) } + } -func TestFields(t *testing.T) { - a := newApp(8326) - if len(a.Password) == 0 { - t.Skip() +func TestDeleteCursor(t *testing.T) { + testData := GetTestDataDeleteCursor() + app := newApp() + err := app.DeleteCursor(testData.input[0].(string)) + if err != nil { + t.Errorf("TestDeleteCursor is failed: %v", err) } +} + +func TestCreateCursor(t *testing.T) { + testData := GetTestDataCreateCursor() + app := newApp() + _, err := app.CreateCursor(testData.input[0].([]string), testData.input[1].(string), uint64(testData.input[2].(int))) + if err != nil { + t.Errorf("TestCreateCurSor is failed: %v", err) + } +} - fi, err := a.Fields() +func TestFields(t *testing.T) { + app := newApp() + fi, err := app.Fields() if err != nil { t.Error("Fields failed", err) } @@ -187,50 +392,43 @@ func TestFields(t *testing.T) { } func TestApiToken(t *testing.T) { - a := newAppWithApiToken(9974) - if len(a.ApiToken) == 0 { - t.Skip() - } - - _, err := a.Fields() + app := newAppWithToken() + _, err := app.Fields() if err != nil { t.Error("Api token failed", err) } } func TestGuestSpace(t *testing.T) { - a := newAppInGuestSpace(185, 9) - if len(a.Password) == 0 { - t.Skip() - } - - _, err := a.Fields() + app := newAppWithGuest() + _, err := app.Fields() if err != nil { t.Error("GuestSpace failed", err) } } func TestGetRecordComments(t *testing.T) { - a := newApp(13) - var offset uint64 = 5 - var limit uint64 = 10 - if rec, err := a.GetRecordComments(3, "asc", offset, limit); err != nil { + testData := GetDataTestRecordComments() + app := newApp() + if rec, err := app.GetRecordComments(uint64(testData.input[0].(int)), testData.input[1].(string), uint64(testData.input[2].(int)), uint64(testData.input[3].(int))); err != nil { t.Error(err) } else { - if !strings.Contains(rec[0].Id, "6") { - t.Errorf("the first comment id mismatch. expected is 6 but actual %v", rec[0].Id) + if !strings.Contains(rec[0].Id, "3") { + t.Errorf("the first comment id mismatch. expected is 3 but actual %v", rec[0].Id) } } } + func TestAddRecordComment(t *testing.T) { - appTest := newApp(12) + testData := GetTestDataAddRecordComment() + appTest := newApp() mentionMemberCybozu := &ObjMention{Code: "cybozu", Type: ConstCommentMentionTypeUser} mentionGroupAdmin := &ObjMention{Code: "Administrators", Type: ConstCommentMentionTypeGroup} mentionDepartmentAdmin := &ObjMention{Code: "Admin", Type: ConstCommentMentionTypeDepartment} var cmt Comment cmt.Text = "Test comment 222" cmt.Mentions = []*ObjMention{mentionGroupAdmin, mentionMemberCybozu, mentionDepartmentAdmin} - cmtID, err := appTest.AddRecordComment(2, &cmt) + cmtID, err := appTest.AddRecordComment(uint64(testData.input[0].(int)), &cmt) if err != nil { t.Error(err) @@ -240,13 +438,21 @@ func TestAddRecordComment(t *testing.T) { } func TestDeleteComment(t *testing.T) { - appTest := newApp(4) - var cmtID uint64 = 14 - err := appTest.DeleteComment(3, 12) - + testData := GetDataTestDeleteRecordComment() + appTest := newApp() + err := appTest.DeleteComment(uint64(testData.input[0].(int)), uint64(testData.input[1].(int))) if err != nil { t.Error(err) } else { - t.Logf("The comment with id = %v has been deleted successefully!", cmtID) + t.Logf("The comment with id = %v has been deleted successefully!", uint64(testData.input[0].(int))) + } +} + +func TestGetProcess(t *testing.T) { + TestData := GetTestDataProcess() + app := newApp() + _, err := app.GetProcess(TestData.input[0].(string)) + if err != nil { + t.Error("TestGetProcess failed: ", err) } } diff --git a/app_test_json.go b/app_test_json.go new file mode 100644 index 0000000..f38d07d --- /dev/null +++ b/app_test_json.go @@ -0,0 +1,419 @@ +package kintone + +import ( + "bytes" +) + +type TestData struct { + input []interface{} + output string +} + +func GetTestDataProcess() *TestData { + return &TestData{ + input: []interface{}{"en"}, + output: ` + { + "enable":true, + "states":{ + "Not started":{ + "name":"Not started", + "index":"0", + "assignee":{ + "type":"ONE", + "entities":[] + } + }, + "In progress":{ + "name":"In progress", + "index":"1", + "assignee":{ + "type":"ALL", + "entities":[ + { + "entity":{ + "type":"USER", + "code":"user1" + }, + "includeSubs":false + }, + { + "entity":{ + "type":"FIELD_ENTITY", + "code":"creator" + }, + "includeSubs":false + }, + { + "entity":{ + "type":"CUSTOM_FIELD", + "code":"Boss" + }, + "includeSubs":false + } + ] + } + }, + "Completed":{ + "name":"Completed", + "index":"2", + "assignee":{ + "type":"ONE", + "entities":[] + } + } + }, + "actions":[ + { + "name":"Start", + "from":"Not started", + "to":"In progress", + "filterCond":"Record_number = \"1\"" + }, + { + "name":"Complete", + "from":"In progress", + "to":"Completed", + "filterCond":"" + } + ], + "revision":"3" + }`, + } +} + +func GetTestDataDeleteRecords() *TestData { + return &TestData{ + input: []interface{}{[]uint64{6, 7}}, + output: `{}`, + } +} +func GetTestDataGetRecord() *TestData { + return &TestData{ + input: []interface{}{1, true}, + output: ` + { + "record":{ + "Updated_by":{ + "type":"MODIFIER", + "value":{ + "code":"Administrator", + "name":"Administrator" + }, + "key":"hehehe" + }, + "$id":{ + "type":"__ID__", + "value":"1" + } + } + }`, + } +} + +func GetTestDataGetRecords() *TestData { + return &TestData{ + input: []interface{}{ + []string{}, + "limit 3 offset 3", + }, + output: ` + { + "records":[ + { + "Created_datetime":{ + "type":"CREATED_TIME", + "value":"2019-03-11T04:50:00Z" + }, + "Created_by":{ + "type":"CREATOR", + "value":{ + "code":"Administrator", + "name":"Administrator" + } + }, + "$id":{ + "type":"__ID__", + "value":"1" + } + }, + { + "Created_datetime":{ + "type":"CREATED_TIME", + "value":"2019-03-11T06:42:00Z" + }, + "Created_by":{ + "type":"CREATOR", + "value":{ + "code":"Administrator", + "name":"Administrator" + } + }, + "$id":{ + "type":"__ID__", + "value":"2" + } + } + ], + "totalCount":null + }`, + } +} + +func GetDataTestUploadFile() *TestData { + return &TestData{ + output: ` + { + "app":3, + "id":6, + "record":{ + "attached_file":{ + "value":[ + { + "fileKey":" c15b3870-7505-4ab6-9d8d-b9bdbc74f5d6" + } + ] + } + } + }`, + } +} + +func GetDataTestRecordComments() *TestData { + return &TestData{ + input: []interface{}{1, "asc", 0, 10}, + output: ` + { + "comments":[ + { + "id":"3", + "text":"user14 Thank you! Looks great.", + "createdAt":"2016-05-09T18:29:05Z", + "creator":{ + "code":"user13", + "name":"user13" + }, + "mentions":[ + { + "code":"user14", + "type":"USER" + } + ] + }, + { + "id":"2", + "text":"user13 Global Sales APAC Taskforce \nHere is today's report.", + "createdAt":"2016-05-09T18:27:54Z", + "creator":{ + "code":"user14", + "name":"user14" + }, + "mentions":[ + { + "code":"user13", + "type":"USER" + }, + { + "code":"Global Sales_1BNZeQ", + "type":"ORGANIZATION" + }, + { + "code":"APAC Taskforce_DJrvzu", + "type":"GROUP" + } + ] + } + ], + "older":false, + "newer":false + }`, + } +} + +func GetDataTestForm() *TestData { + return &TestData{ + output: ` + { + "properties":[ + { + "code":"string_1", + "defaultValue":"", + "expression":"", + "hideExpression":"false", + "maxLength":"64", + "minLength":null, + "label":"string_1", + "noLabel":"false", + "required":"true", + "type":"SINGLE_LINE_TEXT", + "unique":"true" + }, + { + "code":"number_1", + "defaultValue":"12345", + "digit":"true", + "displayScale":"4", + "expression":"", + "maxValue":null, + "minValue":null, + "label":"number_1", + "noLabel":"true", + "required":"false", + "type":"NUMBER", + "unique":"false" + }, + { + "code":"checkbox_1", + "defaultValue":[ + "sample1", + "sample3" + ], + "label":"checkbox_1", + "noLabel":"false", + "options":[ + "sample1", + "sample2", + "sample3" + ], + "required":"false", + "type":"CHECK_BOX" + } + ] + }`, + } +} + +func GetDataTestDeleteRecordComment() *TestData { + return &TestData{ + input: []interface{}{3, 14}, + output: `{}`, + } +} +func GetTestDataAddRecord() *TestData { + return &TestData{ + output: `{ + "id": "1", + "revision": "1" + }`, + } +} + +func GetTestDataAddRecords() *TestData { + return &TestData{ + output: ` + { + "ids": ["77","78"], + "revisions": ["1","1"] + }`, + } +} + +func GetDataTestAddRecord() *TestData { + return &TestData{ + input: []interface{}{ + "ほげ春巻.txta", + bytes.NewReader([]byte(`abc + hoge + `)), + "text/html", + }, + output: ` + { + "id": "1", + "revision": "1" + }`, + } +} +func getDataTestCreateCursor() *TestData { + return &TestData{ + output: ` + { + "id": "9a9716fe-1394-4677-a1c7-2199a5d28215", + "totalCount": 123456 + }`, + } + +} +func GetDataTestGetRecordsByCursor() *TestData { + + return &TestData{ + input: []interface{}{"9a9716fe-1394-4677-a1c7-2199a5d28215"}, + output: ` + { + "records":[ + { + "$id":{ + "type":"__ID__", + "value":"1" + }, + "Created_by":{ + "type":"CREATOR", + "value":{ + "code":"Administrator", + "name":"Administrator" + } + }, + "Created_datetime":{ + "type":"CREATED_TIME", + "value":"2019-05-23T04:50:00Z" + } + } + ], + "next":false + }`, + } +} + +func GetTestDataDeleteCursor() *TestData { + return &TestData{ + input: []interface{}{"9a9716fe-1394-4677-a1c7-2199a5d28215"}, + output: `{}`, + } +} + +func GetTestDataCreateCursor() *TestData { + return &TestData{ + input: []interface{}{[]string{"$id", "date"}, "", 100}, + output: `{"id":"9a9716fe-1394-4677-a1c7-2199a5d28215"}`, + } +} + +func GetTestDataAddRecordComment() *TestData { + return &TestData{ + input: []interface{}{2}, + output: `{"id": "4"}`, + } +} +func GetTestDataUpdateRecordByKey() *TestData { + return &TestData{ + input: []interface{}{2, "key", true}, + output: ` + { + "app":1, + "records":[ + { + "updateKey":{ + "field":"unique_key", + "value":"CODE123" + }, + "record":{ + "Text":{ + "value":"Silver plates" + } + } + }, + { + "updateKey":{ + "field":"unique_key", + "value":"CODE456" + }, + "record":{ + "Text":{ + "value":"The quick brown fox." + } + } + } + ] + }`, + } +} diff --git a/cursor.go b/cursor.go new file mode 100644 index 0000000..9494987 --- /dev/null +++ b/cursor.go @@ -0,0 +1,39 @@ +package kintone + +import ( + "encoding/json" +) + +//Object Cursor structure +type Cursor struct { + Id string `json:"id"` + TotalCount string `json:"totalCount"` +} +type GetRecordsCursorResponse struct { + Records []*Record `json:"records"` + Next bool `json:"next"` +} + +//decodeCursor decodes JSON response for cursor api +func decodeCursor(b []byte) (c *Cursor, err error) { + err = json.Unmarshal(b, &c) + if err != nil { + return nil, err + } + return c, nil +} +func DecodeGetRecordsCursorResponse(b []byte) (rc *GetRecordsCursorResponse, err error) { + var t struct { + Next bool `json:"next"` + } + err = json.Unmarshal(b, &t) + if err != nil { + return nil, err + } + listRecord, err := DecodeRecords(b) + if err != nil { + return nil, err + } + getRecordsCursorResponse := &GetRecordsCursorResponse{Records: listRecord, Next: t.Next} + return getRecordsCursorResponse, nil +} diff --git a/cursor_test.go b/cursor_test.go new file mode 100644 index 0000000..112bb24 --- /dev/null +++ b/cursor_test.go @@ -0,0 +1,13 @@ +package kintone + +import ( + "testing" +) + +func TestDecodeCursor(t *testing.T) { + data := []byte(`{"id":"aaaaaaaaaaaaaaaaaa","totalCount":"null"}`) + _, err := decodeCursor(data) + if err != nil { + t.Errorf("TestDecodeCursor is failed: %v", err) + } +} diff --git a/record.go b/record.go index 88f93b1..e2ec6c3 100644 --- a/record.go +++ b/record.go @@ -324,6 +324,7 @@ func DecodeRecords(b []byte) ([]*Record, error) { if err != nil { return nil, errors.New("Invalid JSON format") } + rec_list := make([]*Record, len(t.Records)) for i, rd := range t.Records { r, err := decodeRecordData(rd)