pivotgrid-js 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/demo_data/demo-config.js +16 -15
- package/demo_data/demo-data.js +1 -1
- package/dist/pivotgrid.cjs.js +939 -929
- package/dist/pivotgrid.esm.js +939 -929
- package/dist/pivotgrid.js +939 -929
- package/package.json +50 -50
- package/providers/rest-provider.js +20 -10
- package/server/README.md +25 -1
- package/server/configs/main_config.json +1 -1
- package/server/server.py +34 -3
- package/src/pivot.js +919 -919
package/package.json
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "pivotgrid-js",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Vanilla JS pivot table — no dependencies, no frameworks",
|
|
5
|
-
"author": "Aleksandr Korolev <korolevalexa@gmail.com>",
|
|
6
|
-
"license": "SEE LICENSE IN LICENSE",
|
|
7
|
-
"homepage": "https://github.com/AlexKorole/pivotgrid",
|
|
8
|
-
"repository": {
|
|
9
|
-
"type": "git",
|
|
10
|
-
"url": "https://github.com/AlexKorole/pivotgrid.git"
|
|
11
|
-
},
|
|
12
|
-
"keywords": [
|
|
13
|
-
"pivot",
|
|
14
|
-
"grid",
|
|
15
|
-
"table",
|
|
16
|
-
"pivot-table",
|
|
17
|
-
"data",
|
|
18
|
-
"analytics",
|
|
19
|
-
"vanilla-js"
|
|
20
|
-
],
|
|
21
|
-
"main": "dist/pivotgrid.cjs.js",
|
|
22
|
-
"module": "dist/pivotgrid.esm.js",
|
|
23
|
-
"exports": {
|
|
24
|
-
".": {
|
|
25
|
-
"import": "./dist/pivotgrid.esm.js",
|
|
26
|
-
"require": "./dist/pivotgrid.cjs.js"
|
|
27
|
-
},
|
|
28
|
-
"./css": "./dist/pivotgrid.css"
|
|
29
|
-
},
|
|
30
|
-
"files": [
|
|
31
|
-
"dist",
|
|
32
|
-
"config",
|
|
33
|
-
"src",
|
|
34
|
-
"engine",
|
|
35
|
-
"providers",
|
|
36
|
-
"widget",
|
|
37
|
-
"server",
|
|
38
|
-
"demo_data",
|
|
39
|
-
"LICENSE",
|
|
40
|
-
"LICENSE.commercial",
|
|
41
|
-
"README.md"
|
|
42
|
-
],
|
|
43
|
-
"scripts": {
|
|
44
|
-
"build": "node build.js",
|
|
45
|
-
"prepublishOnly": "npm run build"
|
|
46
|
-
},
|
|
47
|
-
"devDependencies": {
|
|
48
|
-
"esbuild": "^0.20.2"
|
|
49
|
-
}
|
|
50
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "pivotgrid-js",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Vanilla JS pivot table — no dependencies, no frameworks",
|
|
5
|
+
"author": "Aleksandr Korolev <korolevalexa@gmail.com>",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
7
|
+
"homepage": "https://github.com/AlexKorole/pivotgrid",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/AlexKorole/pivotgrid.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"pivot",
|
|
14
|
+
"grid",
|
|
15
|
+
"table",
|
|
16
|
+
"pivot-table",
|
|
17
|
+
"data",
|
|
18
|
+
"analytics",
|
|
19
|
+
"vanilla-js"
|
|
20
|
+
],
|
|
21
|
+
"main": "dist/pivotgrid.cjs.js",
|
|
22
|
+
"module": "dist/pivotgrid.esm.js",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"import": "./dist/pivotgrid.esm.js",
|
|
26
|
+
"require": "./dist/pivotgrid.cjs.js"
|
|
27
|
+
},
|
|
28
|
+
"./css": "./dist/pivotgrid.css"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"config",
|
|
33
|
+
"src",
|
|
34
|
+
"engine",
|
|
35
|
+
"providers",
|
|
36
|
+
"widget",
|
|
37
|
+
"server",
|
|
38
|
+
"demo_data",
|
|
39
|
+
"LICENSE",
|
|
40
|
+
"LICENSE.commercial",
|
|
41
|
+
"README.md"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "node build.js",
|
|
45
|
+
"prepublishOnly": "npm run build"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"esbuild": "^0.20.2"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -271,19 +271,29 @@ class RestProvider {
|
|
|
271
271
|
// ── HTTP ───────────────────────────────────────────────────────────────────
|
|
272
272
|
|
|
273
273
|
async _execute(query) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
274
|
+
let page = 0;
|
|
275
|
+
let allRows = [];
|
|
276
|
+
let hasMore = true;
|
|
277
|
+
|
|
278
|
+
while (hasMore) {
|
|
279
|
+
const res = await fetch(this.url, {
|
|
280
|
+
method: 'POST',
|
|
281
|
+
headers: { 'Content-Type': 'application/json' },
|
|
282
|
+
body: JSON.stringify({ query, page }),
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
if (!res.ok) {
|
|
286
|
+
const err = await res.json().catch(() => ({}));
|
|
287
|
+
throw new Error(`Server error ${res.status}: ${err.error || ''}`);
|
|
288
|
+
}
|
|
279
289
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
290
|
+
const data = await res.json();
|
|
291
|
+
allRows = allRows.concat(data.rows);
|
|
292
|
+
hasMore = data.hasMore;
|
|
293
|
+
page++;
|
|
283
294
|
}
|
|
284
295
|
|
|
285
|
-
|
|
286
|
-
return rows.map(row => {
|
|
296
|
+
return allRows.map(row => {
|
|
287
297
|
const out = {};
|
|
288
298
|
for (const k of Object.keys(row)) out[k.toLowerCase()] = row[k];
|
|
289
299
|
return out;
|
package/server/README.md
CHANGED
|
@@ -42,7 +42,7 @@ All settings can also be configured via the Config Editor UI.
|
|
|
42
42
|
|
|
43
43
|
| Method | Path | Description |
|
|
44
44
|
|--------|------|-------------|
|
|
45
|
-
| `POST` | `/query` | Execute a SELECT query, returns JSON
|
|
45
|
+
| `POST` | `/query` | Execute a SELECT query, returns paginated JSON (`{rows, total, page, hasMore}`) |
|
|
46
46
|
| `GET` | `/configs` | List all config names |
|
|
47
47
|
| `GET` | `/configs/{name}` | Get a config by name |
|
|
48
48
|
| `POST` | `/configs/{name}` | Save a config by name |
|
|
@@ -50,6 +50,30 @@ All settings can also be configured via the Config Editor UI.
|
|
|
50
50
|
| `POST` | `/server-config` | Save DB settings to `.env` |
|
|
51
51
|
| `POST` | `/test-connection` | Test DB connection with given credentials |
|
|
52
52
|
|
|
53
|
+
## Query Pagination
|
|
54
|
+
|
|
55
|
+
Large result sets are split into pages to avoid sending huge HTTP responses
|
|
56
|
+
(which can fail or time out on slower connections).
|
|
57
|
+
|
|
58
|
+
The query runs **once** — results are cached in memory on the server and
|
|
59
|
+
sliced into pages as the client requests them, so a single `GROUP BY` is
|
|
60
|
+
never re-executed for each page.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
_PAGE_SIZE = 200_000 # rows per page sent to the client
|
|
64
|
+
_QUERY_CACHE_TTL = 300 # seconds a query's results stay in server memory
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- `_PAGE_SIZE` — increase if your network/hardware comfortably handles
|
|
68
|
+
larger responses; decrease if you see slow or failing requests on big
|
|
69
|
+
datasets.
|
|
70
|
+
- `_QUERY_CACHE_TTL` — how long the server keeps a query's full result in
|
|
71
|
+
memory while the client fetches subsequent pages. Stale entries are
|
|
72
|
+
cleaned up automatically.
|
|
73
|
+
|
|
74
|
+
The client (`RestProvider`) handles pagination transparently — it requests
|
|
75
|
+
pages in a loop and concatenates them into a single array.
|
|
76
|
+
|
|
53
77
|
## Configs
|
|
54
78
|
|
|
55
79
|
Configs are stored as JSON files in `server/configs/`:
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
],
|
|
107
107
|
"measure": "revenue",
|
|
108
108
|
"func": "sum",
|
|
109
|
-
"maxCachedRows":
|
|
109
|
+
"maxCachedRows": 1000000,
|
|
110
110
|
"filterCheckboxLimit": 30,
|
|
111
111
|
"drillthroughQuery": "\n SELECT ID,REGION FROM sales_data\n WHERE {filters}\n LIMIT 200 OFFSET 0\n "
|
|
112
112
|
}
|
package/server/server.py
CHANGED
|
@@ -20,6 +20,8 @@ Start:
|
|
|
20
20
|
python server.py
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
|
+
import time
|
|
24
|
+
import hashlib
|
|
23
25
|
import json
|
|
24
26
|
import gzip
|
|
25
27
|
import os
|
|
@@ -38,6 +40,18 @@ CONNECTORS_DIR = os.path.join(BASE_DIR, 'connectors')
|
|
|
38
40
|
|
|
39
41
|
os.makedirs(CONFIGS_DIR, exist_ok=True)
|
|
40
42
|
|
|
43
|
+
_QUERY_CACHE = {} # query hash → {'rows': [...], 'ts': last access time}
|
|
44
|
+
_QUERY_CACHE_TTL = 300 # seconds — how long a cached query result stays in memory
|
|
45
|
+
_PAGE_SIZE = 200_000 # rows per page sent to the client in one HTTP response
|
|
46
|
+
|
|
47
|
+
def _cache_key(query):
|
|
48
|
+
return hashlib.sha256(query.encode('utf-8')).hexdigest()
|
|
49
|
+
|
|
50
|
+
def _cleanup_cache():
|
|
51
|
+
now = time.time()
|
|
52
|
+
for k in [k for k, v in _QUERY_CACHE.items() if now - v['ts'] > _QUERY_CACHE_TTL]:
|
|
53
|
+
del _QUERY_CACHE[k]
|
|
54
|
+
|
|
41
55
|
# ── Load .env ──────────────────────────────────────────────────────────────────
|
|
42
56
|
|
|
43
57
|
def load_env(path):
|
|
@@ -237,10 +251,27 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
237
251
|
self._send(400, json.dumps({'error': 'Only SELECT allowed'}))
|
|
238
252
|
return
|
|
239
253
|
|
|
254
|
+
page = int(payload.get('page', 0))
|
|
255
|
+
key = _cache_key(query)
|
|
256
|
+
|
|
240
257
|
try:
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
258
|
+
if page == 0 or key not in _QUERY_CACHE:
|
|
259
|
+
_cleanup_cache()
|
|
260
|
+
connector = get_active_connector()
|
|
261
|
+
rows = connector.execute_query(query)
|
|
262
|
+
_QUERY_CACHE[key] = {'rows': rows, 'ts': time.time()}
|
|
263
|
+
else:
|
|
264
|
+
_QUERY_CACHE[key]['ts'] = time.time()
|
|
265
|
+
|
|
266
|
+
rows = _QUERY_CACHE[key]['rows']
|
|
267
|
+
total = len(rows)
|
|
268
|
+
start = page * _PAGE_SIZE
|
|
269
|
+
chunk = rows[start:start + _PAGE_SIZE]
|
|
270
|
+
has_more = start + _PAGE_SIZE < total
|
|
271
|
+
|
|
272
|
+
self._send(200, json.dumps({
|
|
273
|
+
'rows': chunk, 'total': total, 'page': page, 'hasMore': has_more,
|
|
274
|
+
}, ensure_ascii=False, default=str))
|
|
244
275
|
except Exception as e:
|
|
245
276
|
self._send(500, json.dumps({'error': str(e)}))
|
|
246
277
|
|