Skip to content

Commit 20d2ab8

Browse files
authored
Release v1.2 (#30)
* Support additional operators in field based search * Add index on table lines * Add index on account ID to lines * Remove unnecessary logs * Remove unnecessary logs * Introduce in and is operators in range * Fix issue with null values * Add test cases for in and is operators * Sort transactions by timestamp * Fix lines sorting * test uuid table * Remove test table * Fix docs * Fix docs * Fix docs * Remove QBank specific indexes * Add docs on environment variables
1 parent 2cc68f5 commit 20d2ab8

15 files changed

+419
-56
lines changed

README.md

Lines changed: 83 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,19 @@ Systems that manage money do so by managing its movement - by tracking where it
55

66
The there are two primitives in the system: **accounts** and **transactions**. Money moves between accounts by means of a transaction.
77

8-
A **transaction** may have multiple *lines* - each line represents the change (*delta*) of money in one account. A valid transaction has a total delta of zero - no money is created or destroyed, and all money moved out of any account(s) has moved in to other account(s). QLedger validates all transactions made via the API with a zero delta check.
8+
A **transaction** may have multiple *lines* - each line represents the change (*delta*) of money in one *account*. A valid transaction has a total delta of zero - no money is created or destroyed, and all money moved out of any account(s) has moved in to other account(s). QLedger validates all transactions made via the API with a zero delta check.
99

1010
> Phrased another way, the law of conversation of money is formalized by the rules of double entry bookkeeping - money debited from any account must be credited to another account (and vice versa), implying that all transactions must have at least two entries (double entry) with a zero sum delta. QLedger makes it easy to follow these rules.
1111
1212
Accounts do not need to be predefined - they are called into existence when they are first used.
1313

14-
All accounts and transactions are identified by a string identifier, which also acts an idempotency and an immutability key. Transactions once sent to the ledger cannot be changed - any 'modification' or reversal requires a new transaction. The safe recovery mechanism for all network errors is also a simple retry - as long as the identifier does not change the transaction will never be indavertently duplicated.
14+
All accounts and transactions are identified by a string identifier, which also acts an idempotency and an immutability key. Transactions once sent to the ledger cannot be changed - any 'modification' or reversal requires a new transaction. The safe recovery mechanism for all network errors is also a simple retry - as long as the identifier does not change the transaction will never be inadvertently duplicated.
1515

16-
#### POST `/v1/transactions`
16+
## Transactions
17+
18+
Transaction can be created as follows:
19+
20+
`POST /v1/transactions`
1721
```
1822
{
1923
"id": "abcd1234",
@@ -32,27 +36,37 @@ All accounts and transactions are identified by a string identifier, which also
3236
```
3337
> Transactions with a total delta not equal to zero will result in a `400 BAD REQUEST` error.
3438
39+
Transaction `timestamp` by default will be the time at which it is created. If necessary(such as migration of existing
40+
transactions), can be overridden using the `timestamp` property in the payload as follows:
41+
42+
`POST /v1/transactions`
43+
```
44+
{
45+
"id": "abcd1234",
46+
"timestamp": "2017-01-01 13:01:05.000",
47+
...
3548
36-
#### Metadata for Querying and Reports
49+
}
50+
```
3751

38-
Transactions and accounts can have arbitrary number of key-value pairs maintained as a single JSON `data` which helps in grouping and filtering them by one or more criteria.
52+
> The `timestamp` in the payload should be in the format `2006-01-02 15:04:05.000`.
3953
40-
Both transactions and accounts can be updated multiple times with `data`. The existing `data` is always overwritten with the new `data` value.
54+
Transactions can have arbitrary number of key-value pairs maintained as a single JSON `data` which helps in grouping and filtering them by one or more criteria.
4155

4256
The `data` can be arbitrary JSON value as follows:
4357
```
4458
{
4559
"data": {
46-
"k1": "",
47-
"k2": "strval",
48-
"k3": ["av1", "av2", "av3"],
49-
"k4": {
50-
"nest1": {
51-
"nest2": "val"
60+
"active": true,
61+
"status": "completed",
62+
"codes": ["O123", "C123", "F123"],
63+
"client_data": {
64+
"interval": {
65+
"invoice": "monthly"
5266
}
5367
},
54-
"k5": 2017,
55-
"k6": "2017-12-01"
68+
"amount": 2000,
69+
"expiry": "2017-12-01T05:00:00Z"
5670
}
5771
}
5872
```
@@ -87,20 +101,7 @@ The transactions can be created with `data` as follows:
87101
}
88102
```
89103

90-
The accounts can be created with `data` as follows:
91-
92-
`POST /v1/accounts`
93-
```
94-
{
95-
"id": "alice",
96-
"data": {
97-
"product": "qw",
98-
"date": "2017-01-01"
99-
}
100-
}
101-
```
102-
103-
The transactions or accounts can be updated with `data` using endpoints `PUT /v1/transactions` and `PUT /v1/accounts`
104+
Transactions can be updated multiple times with `data`. The existing `data` is always overwritten with the new `data` value.
104105

105106
The transaction with ID `abcd1234` is updated with `data` as follows:
106107

@@ -125,24 +126,56 @@ The transaction with ID `abcd1234` is updated with `data` as follows:
125126
}
126127
```
127128

128-
#### Searching of accounts and transactions
129+
## Accounts
130+
131+
An account with ID `alice` can be created with `data` as follows:
132+
133+
`POST /v1/accounts`
134+
```
135+
{
136+
"id": "alice",
137+
"data": {
138+
"product": "qw",
139+
"date": "2017-01-01"
140+
}
141+
}
142+
```
143+
144+
An account can be updated with `data` as follows:
145+
146+
`PUT /v1/accounts`
147+
```
148+
{
149+
"id": "alice",
150+
"data": {
151+
"product": "qw",
152+
"date": "2017-01-05"
153+
}
154+
}
155+
```
156+
157+
## Searching of accounts and transactions
129158

130159
The transactions and accounts can be filtered from the endpoints `GET /v1/transactions` and `GET /v1/accounts` with the search query formed using the bool clauses(`must` and `should`) and query types(`fields`, `terms` and `ranges`).
131160

132-
##### Query types:
161+
### Query types:
133162

134-
###### - `fields` query
163+
##### `fields` query
135164

136165
Find items where the specified column exists with the specified value in the specified range.
137166

138167
Example fields:
139168
- Field `{"id": {"eq": "ACME.CREDIT"}}` filters items where the column `id` is equal to `ACME.CREDIT`
169+
- Field `{"balance": {"ne": 0}}` filters items where the column `balance` is not equal to `0`.
140170
- Field `{"balance": {"lt": 0}}` filters items where the column `balance` is less than `0`
141171
- Field `{"timestamp": {"gte": "2017-01-01T05:30"}}` filters items where `timestamp` is greater than or equal to `2017-01-01T05:30`
172+
- Field `{"id": {"ne": "ACME.CREDIT"}}` filters items where the column `id` is not equal to `ACME.CREDIT`
173+
- Field `{"id": {"like": "%.DEBIT"}}` filters items where the column `id` ends with `.DEBIT`
174+
- Field `{"id": {"notlike": "%.DEBIT"}}` filters items where the column `id` doesn't ends with `.DEBIT`
142175

143-
> The supported range operators are `lt`(less than), `lte`(less than or equal), `gt`(greater than), `gte`(greater than or equal), `eq`(equal).
176+
> The supported field operators are `lt`(less than), `lte`(less than or equal), `gt`(greater than), `gte`(greater than or equal), `eq`(equal), `ne`(not equal), `like`(like patterns), `notlike`(not like patterns).
144177
145-
###### - `terms` query
178+
##### `terms` query
146179

147180
Filters items where the specified key-value pairs in a term exists in the `data` JSON.
148181

@@ -151,20 +184,25 @@ Example terms:
151184
- Term `{"months": ["jan", "feb", "mar"]}` filters items where values `jan`, `feb` AND `mar` in `data.months` array
152185
- Term `{"products":{"qw":{"tax":18.0}}}` filters items where subset `{"qw": {"tax": 18.0}}` in `products` object
153186

154-
155-
###### - `range` query
187+
##### `range` query
156188

157189
Filters items which the specified key in `data` JSON exists in the specified range of values.
158190

159191
Example range:
160192
- Range `{"charge": {"gte": 2000, "lte": 4000}}` filters items where `data.charge >= 2000` AND `data.charge <= 4000`
161193
- Range `{"date": {"gt": "2017-01-01","lt": "2017-06-31"}}` filters items where `data.date > '2017-01-01'` AND `data.date < '2017-01-31'`
194+
- Range `{"type": {"is": null}}` filters items where `data.type` is `NIL`
195+
- Range `{"type": {"is": null}}` filters items where `data.type` is not `NIL`
196+
- Range `{"action": {"in": ["intent", "invoice"]}}` filters items where `data.action` is ANY of `("intent", "invoice")`
197+
- Range `{"action": {"nin": ["charge", "refund"]}}` filters items where `data.action` is NOT ANY of `("charge", "refund")`
162198

199+
> The supported range operators are `lt`(less than), `lte`(less than or equal), `gt`(greater than), `gte`(greater than or equal), `eq`(equal), `ne`(not equal), `like`(like patterns), `notlike`(not like patterns), `is`(is null checks), `isnot`(not null checks), `in`(ANY of list), `nin`(NOT ANY of list).
163200
164-
##### Bool clauses:
201+
202+
### Bool clauses:
165203
The following bool clauses determine whether all or any of the queries needs to be satisfied.
166204

167-
###### - `must` clause
205+
##### `must` clause
168206
All of the query items in the `must` clause must be satisfied to get results.
169207

170208
> The `must` clause can be equated with boolean `AND`
@@ -198,9 +236,7 @@ Example: The following query matches requests to match accounts which satisfies
198236
}
199237
```
200238

201-
202-
203-
###### - `should` clause
239+
##### `should` clause
204240
Any of the query items in the `should` clause should be satisfied to get results.
205241

206242
> The `should` clause can be equated with boolean `OR`
@@ -238,3 +274,10 @@ Example: The following query matches requests to match transactions which satisf
238274
- Clients those doesn't support passing search payload in the `GET`, can alternatively use the `POST` endpoints: `POST /v1/transactions/_search` and `POST /v1/accounts/_search`.
239275

240276
- A search query can have both `must` and `should` clauses.
277+
278+
- Transactions in the search result are ordered chronological by default.
279+
280+
281+
## Environment Variables:
282+
283+
Please read the documentation of all QLedger environment variables [here](./context#environment-variables)

context/README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
## Environment Variables
2+
3+
#### Server Port: [Optional]
4+
5+
QLedger server by default runs in port `7000`, which can be overridden by the following:
6+
```
7+
export PORT=7000
8+
```
9+
10+
#### Authentication Token:
11+
12+
QLedger API requests are authenticated using the secret token, which can be set using the following:
13+
```
14+
export LEDGER_AUTH_TOKEN=XXXXX
15+
```
16+
17+
#### Database URL:
18+
19+
QLedger uses PostgreSQL database to store the accounts and transactions.
20+
21+
The PostgreSQL database URL can be set using:
22+
```
23+
export DATABASE_URL="postgres://localhost/ledgerdb?sslmode=disable"
24+
```
25+
26+
For the purpose of running test cases, a separate database URL can be set using:
27+
```
28+
export TEST_DATABASE_URL="postgres://localhost/qw_ledger_test?sslmode=disable"
29+
```
30+
31+
**Note:**
32+
33+
- The database URL can be in one of the mentioned formats here:
34+
https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING
35+
36+
#### Sharing Load Balancer/Domain Name: [Optional]
37+
38+
In staging/production environments, the services are usually deployed in the same domain, differentiated and routed using the definite path prefixes.
39+
40+
To access all QLedger APIs with prefix `/qledger/api`, set the following:
41+
```
42+
export HOST_PREFIX=/qledger/api
43+
```

controllers/accounts.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ func GetAccounts(w http.ResponseWriter, r *http.Request, context *ledgerContext.
2222
}
2323
defer r.Body.Close()
2424
query := string(body)
25-
log.Println("Query:", query)
2625

2726
engine, aerr := models.NewSearchEngine(context.DB, models.SearchNamespaceAccounts)
2827
if aerr != nil {

controllers/transactions.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func MakeTransaction(w http.ResponseWriter, r *http.Request, context *ledgerCont
8383
}
8484
// Otherwise the transaction is just a duplicate
8585
// The exactly duplicate transactions are ignored
86-
log.Println("Transaction is duplicate:", transaction.ID)
86+
// log.Println("Transaction is duplicate:", transaction.ID)
8787
w.WriteHeader(http.StatusAccepted)
8888
return
8989
}
@@ -115,7 +115,6 @@ func GetTransactions(w http.ResponseWriter, r *http.Request, context *ledgerCont
115115
return
116116
}
117117
query := string(body)
118-
log.Println("Query:", query)
119118

120119
results, aerr := engine.Query(query)
121120
if aerr != nil {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DROP INDEX IF EXISTS lines_transaction_id_idx;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CREATE INDEX lines_transaction_id_idx ON lines USING btree (transaction_id);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DROP INDEX IF EXISTS lines_account_id_idx;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CREATE INDEX lines_account_id_idx ON lines USING btree (account_id);

models/search.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"database/sql"
55
"encoding/json"
66
"errors"
7-
"log"
87
"regexp"
98
"strconv"
109
"strings"
@@ -63,8 +62,6 @@ func (engine *SearchEngine) Query(q string) (interface{}, ledgerError.Applicatio
6362
}
6463

6564
sqlQuery := rawQuery.ToSQLQuery(engine.namespace)
66-
log.Println("sqlQuery SQL:", sqlQuery.sql)
67-
log.Println("sqlQuery args:", sqlQuery.args)
6865
rows, err := engine.db.Query(sqlQuery.sql, sqlQuery.args...)
6966
if err != nil {
7067
return nil, DBError(err)
@@ -259,6 +256,10 @@ func (rawQuery *SearchRawQuery) ToSQLQuery(namespace string) *SearchSQLQuery {
259256
q = q + "(" + strings.Join(shouldWhere, " OR ") + ")"
260257
}
261258

259+
if namespace == "transactions" {
260+
q = q + " ORDER BY timestamp"
261+
}
262+
262263
if offset > 0 {
263264
q = q + " OFFSET " + strconv.Itoa(offset) + " "
264265
}

0 commit comments

Comments
 (0)