-
Notifications
You must be signed in to change notification settings - Fork 0
/
session.go
140 lines (126 loc) · 4.85 KB
/
session.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
package odoo
import (
"context"
"errors"
"fmt"
"io"
"net/http"
)
var (
// ErrInvalidCredentials is an error that indicates an authentication error due to missing or invalid credentials.
ErrInvalidCredentials = errors.New("invalid credentials")
)
//go:generate go run github.com/golang/mock/mockgen -destination=./odoomock/$GOFILE -package odoomock github.com/vshn/appuio-odoo-adapter/odoo QueryExecutor
// QueryExecutor runs queries against Odoo API.
type QueryExecutor interface {
// SearchGenericModel accepts a SearchReadModel and unmarshal the response into the given pointer.
// Depending on the JSON fields returned a custom json.Unmarshaler needs to be written since Odoo sets undefined fields to `false` instead of null.
SearchGenericModel(ctx context.Context, model SearchReadModel, into interface{}) error
// CreateGenericModel accepts a payload and executes a query to create the new data record.
CreateGenericModel(ctx context.Context, model string, data interface{}) (int, error)
// UpdateGenericModel accepts a payload and executes a query to update an existing data record.
UpdateGenericModel(ctx context.Context, model string, id int, data interface{}) error
// DeleteGenericModel accepts a model identifier and data records IDs as payload and executes a query to delete multiple existing data records.
// At least one ID is required.
DeleteGenericModel(ctx context.Context, model string, ids []int) error
// ExecuteQuery runs a generic JSONRPC query with the given model as payload and deserializes the response.
ExecuteQuery(ctx context.Context, path string, model interface{}, into interface{}) error
}
// Session information
type Session struct {
// SessionID is the session SessionID.
// Is always set, no matter the authentication outcome.
SessionID string `json:"session_id,omitempty"`
// UID is the user's ID as an int, or the boolean `false` if authentication failed.
UID int `json:"uid,omitempty"`
client *Client
}
// SearchGenericModel implements QueryExecutor.
func (s *Session) SearchGenericModel(ctx context.Context, model SearchReadModel, into interface{}) error {
return s.ExecuteQuery(ctx, "/web/dataset/search_read", model, into)
}
// CreateGenericModel implements QueryExecutor.
func (s *Session) CreateGenericModel(ctx context.Context, model string, data interface{}) (int, error) {
payload := WriteModel{
Model: model,
Method: MethodCreate,
Args: []interface{}{data},
KWArgs: map[string]interface{}{}, // set to non-null when serializing
}
resultID := 0
err := s.ExecuteQuery(ctx, "/web/dataset/call_kw/create", payload, &resultID)
return resultID, err
}
// UpdateGenericModel implements QueryExecutor.
func (s *Session) UpdateGenericModel(ctx context.Context, model string, id int, data interface{}) error {
if id == 0 {
return fmt.Errorf("id cannot be zero: %v", data)
}
payload := WriteModel{
Model: model,
Method: MethodWrite,
Args: []interface{}{
[]int{id},
data,
},
KWArgs: map[string]interface{}{}, // set to non-null when serializing
}
updated := false
err := s.ExecuteQuery(ctx, "/web/dataset/call_kw/write", payload, &updated)
return err
}
// DeleteGenericModel implements QueryExecutor.
func (s *Session) DeleteGenericModel(ctx context.Context, model string, ids []int) error {
if len(ids) == 0 {
return fmt.Errorf("slice of ID(s) is required")
}
for i, id := range ids {
if id == 0 {
return fmt.Errorf("id cannot be zero (index: %d)", i)
}
}
payload := WriteModel{
Model: model,
Method: MethodDelete,
Args: []interface{}{ids},
KWArgs: map[string]interface{}{}, // set to non-null when serializing
}
deleted := false
err := s.ExecuteQuery(ctx, "/web/dataset/call_kw/unlink", payload, &deleted)
return err
}
// ExecuteQuery implements QueryExecutor.
func (s *Session) ExecuteQuery(ctx context.Context, path string, model interface{}, into interface{}) error {
body, err := NewJSONRPCRequest(&model).Encode()
if err != nil {
return newEncodingRequestError(err)
}
// Create request
req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.client.parsedURL.String()+path, body)
if err != nil {
return newCreatingRequestError(err)
}
req.Header.Set("content-type", "application/json")
req.Header.Set("cookie", "session_id="+s.SessionID)
resp, err := s.sendRequest(req)
if err != nil {
return err
}
return s.unmarshalResponse(resp.Body, into)
}
func (s *Session) sendRequest(req *http.Request) (*http.Response, error) {
res, err := s.client.http.Do(req)
if err != nil {
return nil, fmt.Errorf("sending HTTP request: %w", err)
} else if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("expected HTTP status 200 OK, got %s", res.Status)
}
return res, nil
}
func (s *Session) unmarshalResponse(body io.ReadCloser, into interface{}) error {
defer body.Close()
if err := DecodeResult(body, into); err != nil {
return fmt.Errorf("decoding result: %w", err)
}
return nil
}