startgg-helper 1.0.2
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/main.js +2 -0
- package/package.json +34 -0
- package/readme.md +16 -0
- package/src/jsUtil.js +25 -0
- package/src/query.js +133 -0
- package/src/queryLimiter.js +137 -0
package/main.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "startgg-helper",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "A set of functions and classes useful to communicate with the start.gg API, using any client (YOU NEED TO PROVIDE A CLIENT YOURSELF, SEE README)",
|
|
5
|
+
"main": "main.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node --experimental-vm-modules --no-warnings node_modules/jest/bin/jest.js --silent"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://gitlab.com/TwilCynder/startgg-helper.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"start.gg",
|
|
15
|
+
"API",
|
|
16
|
+
"GraphQL"
|
|
17
|
+
],
|
|
18
|
+
"author": "TwilCynder",
|
|
19
|
+
"license": "ISC",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://gitlab.com/TwilCynder/startgg-helper/issues"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://gitlab.com/TwilCynder/startgg-helper#readme",
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@twilcynder/arguments-parser": "^1.16.1",
|
|
26
|
+
"@types/jest": "^29.5.12",
|
|
27
|
+
"graphql-request": "^7.1.0",
|
|
28
|
+
"jest": "^29.7.0"
|
|
29
|
+
},
|
|
30
|
+
"type": "module",
|
|
31
|
+
"jest": {
|
|
32
|
+
"transform": {}
|
|
33
|
+
}
|
|
34
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# start.gg Helper
|
|
2
|
+
|
|
3
|
+
A set of functions and classes useful to interact with the start.gg GraphQL API.
|
|
4
|
+
|
|
5
|
+
## You need to provide a client to interact with the API
|
|
6
|
+
The functions in this package usually take a "client" argument, which is a GraphQL client object. You need to handle this object yourself, which can be done using the `graphql` package, which is not a dependency of `startgg-helper` and must be installed manually.
|
|
7
|
+
|
|
8
|
+
### Why do this ?
|
|
9
|
+
|
|
10
|
+
The purpose of this package is to be usable not onlyin a node ecosystem but also in a browser project, using `browserify`. The `graphql` package does not work well with browserify, so I decided to let the user provide a client depending on their environment.
|
|
11
|
+
|
|
12
|
+
### Ok actually you can just use startgg-helper-node or startgg-helper-browser
|
|
13
|
+
|
|
14
|
+
If you don't want to handle the client yourself, you can simply install
|
|
15
|
+
- `startgg-helper-node` if you're on Node, it includes `graphql` and handles the client using this package
|
|
16
|
+
- `startgg-helper-browser` if you're doing a browser-based app using `browserify`, which provides a custom client class based on the `fetch` API.
|
package/src/jsUtil.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* @param {{}} obj
|
|
4
|
+
* @param {string} path
|
|
5
|
+
* @param {*} def
|
|
6
|
+
* @returns
|
|
7
|
+
*/
|
|
8
|
+
export function deep_get(obj, path, def = null){
|
|
9
|
+
//https://stackoverflow.com/a/8817473
|
|
10
|
+
path = path=path.split('.');
|
|
11
|
+
for (let i = 0; i < path.length; i++){
|
|
12
|
+
if (/^\d/.test(path[i])){
|
|
13
|
+
let n = parseInt(path[i]);
|
|
14
|
+
if (!isNaN){
|
|
15
|
+
path[i] = n;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
for (var i=0, len=path.length; i<len; i++){
|
|
21
|
+
obj = obj[path[i]];
|
|
22
|
+
if (obj == undefined) return def;
|
|
23
|
+
};
|
|
24
|
+
return obj;
|
|
25
|
+
};
|
package/src/query.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { deep_get } from './jsUtil.js';
|
|
2
|
+
import { TimedQuerySemaphore } from './queryLimiter.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Dummy client class that's only used in the JSDoc.
|
|
6
|
+
* In a real use case, the user will have their own client class.
|
|
7
|
+
*/
|
|
8
|
+
export class Client {
|
|
9
|
+
request(){}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class Query {
|
|
13
|
+
#schema;
|
|
14
|
+
#maxTries;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
*
|
|
18
|
+
* @param {string} schema
|
|
19
|
+
* @param {number?} maxTries
|
|
20
|
+
*/
|
|
21
|
+
constructor (schema, maxTries = null){
|
|
22
|
+
this.#schema = schema;
|
|
23
|
+
this.#maxTries = maxTries;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
*
|
|
28
|
+
* @param {string} logName
|
|
29
|
+
* @param {{[varName: string]: value}} params
|
|
30
|
+
* @returns
|
|
31
|
+
*/
|
|
32
|
+
#getLog(logName, params){
|
|
33
|
+
if (!this.log) return null;
|
|
34
|
+
let log = this.log[logName];
|
|
35
|
+
if (log){
|
|
36
|
+
if (typeof log == "string"){
|
|
37
|
+
return log;
|
|
38
|
+
} else if (typeof log == "function"){
|
|
39
|
+
return log(params);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
*
|
|
47
|
+
* @param {Client} client
|
|
48
|
+
* @param {{[varName: string]: value}} params
|
|
49
|
+
* @param {number} tries How many tries in are we
|
|
50
|
+
* @param {TimedQuerySemaphore} limiter
|
|
51
|
+
* @param {boolean} silentErrors
|
|
52
|
+
* @param {number} maxTries Overrides this.#maxTries
|
|
53
|
+
* @returns
|
|
54
|
+
*/
|
|
55
|
+
async #execute_(client, params, tries, limiter = null, silentErrors = false, maxTries = null){
|
|
56
|
+
maxTries = maxTries || this.#maxTries || 1
|
|
57
|
+
|
|
58
|
+
console.log((this.#getLog("query", params) || "Querying ...") + " Try " + (tries + 1));
|
|
59
|
+
try {
|
|
60
|
+
let data = await ( limiter ? limiter.execute(client, this.#schema, params) : client.request(this.#schema, params));
|
|
61
|
+
|
|
62
|
+
return data;
|
|
63
|
+
} catch (e) {
|
|
64
|
+
|
|
65
|
+
if (tries >= maxTries) {
|
|
66
|
+
console.error("Maximum number of tries reached. Throwing.", e);
|
|
67
|
+
throw e;
|
|
68
|
+
}
|
|
69
|
+
console.error((this.#getLog("error", params) || "Request failed.") + ` Retrying (try ${tries + 1}). Error : `, e);
|
|
70
|
+
return this.#execute_(client, params, tries + 1, limiter, silentErrors, maxTries);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Executes the query with given parameters and client
|
|
76
|
+
* @param {Client} client
|
|
77
|
+
* @param {{[varName: string]: value}} params
|
|
78
|
+
* @param {TimedQuerySemaphore} limiter
|
|
79
|
+
* @param {boolean} silentErrors
|
|
80
|
+
* @param {number} maxTries Overrides the default maximum tries count for this query
|
|
81
|
+
* @returns
|
|
82
|
+
*/
|
|
83
|
+
async execute(client, params, limiter = null, silentErrors = false, maxTries = null){
|
|
84
|
+
return await this.#execute_(client, params, 0, limiter, silentErrors, maxTries);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Executes a query containing a paginated collection, repeatedly, increasing the page index each time until nothing is returned, returning an aggregation of all the pages.
|
|
89
|
+
* @param {Client} client
|
|
90
|
+
* @param {{[varName: string]: value}} params
|
|
91
|
+
* @param {string} collectionPathInQuery JSON path to the paginated collection that must aggregated in the query (JSON path : property names separated by dots)
|
|
92
|
+
* @param {TimedQuerySemaphore} limiter
|
|
93
|
+
* @param {number} delay Adds a delay between calls (does not interact with the limiter)
|
|
94
|
+
* @param {string} pageParamName Name of the query parameter that must be updated with a page index for each query
|
|
95
|
+
* @param {boolean} silentErrors
|
|
96
|
+
* @param {number} maxTries
|
|
97
|
+
* @returns
|
|
98
|
+
*/
|
|
99
|
+
async executePaginated(client, params, collectionPathInQuery, limiter = null, delay = null, perPage = undefined, pageParamName = "page", perPageParamName = "perPage", silentErrors = false, maxTries = null){
|
|
100
|
+
let result = [];
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
params = Object.assign({}, params);
|
|
104
|
+
params[pageParamName] = 1;
|
|
105
|
+
params[perPageParamName] = perPage ?? params[perPageParamName];
|
|
106
|
+
|
|
107
|
+
while (true){
|
|
108
|
+
console.log("Querying page", params[pageParamName], `(${result.length} elements loaded)`);
|
|
109
|
+
let data = await this.execute(client, params, limiter, silentErrors, maxTries);
|
|
110
|
+
|
|
111
|
+
if (!data) throw (this.#getLog("error", params) ?? "Request failed.") + "(in paginated execution, at page " + params[pageParamName] + ")";
|
|
112
|
+
|
|
113
|
+
let localResult = deep_get(data, collectionPathInQuery);
|
|
114
|
+
|
|
115
|
+
if (!localResult) {
|
|
116
|
+
console.warn(`The given path ${collectionPathInQuery} does not point to anything.`);
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!localResult.push) throw "The given path does not point to an array."
|
|
121
|
+
|
|
122
|
+
if (localResult.length < 1) break;
|
|
123
|
+
|
|
124
|
+
result = result.concat(localResult);
|
|
125
|
+
params[pageParamName]++;
|
|
126
|
+
|
|
127
|
+
if (delay)
|
|
128
|
+
await new Promise(r => setTimeout(r, delay));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
//i am a god of JS
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dummy client class that's only used in the JSDoc.
|
|
5
|
+
* In a real use case, the user will have their own client class.
|
|
6
|
+
*/
|
|
7
|
+
export class Client {
|
|
8
|
+
request(){}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class TimedQuerySemaphore {
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {{client: Client, schema: string,
|
|
14
|
+
* params: {[varName: string]: value},
|
|
15
|
+
* resolve: (value: any) => void,
|
|
16
|
+
* reject: (reason?: any) => void}
|
|
17
|
+
* } Query
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**@type {Query[]} */
|
|
21
|
+
#queue = [];
|
|
22
|
+
|
|
23
|
+
/**@type {NodeJS.Timeout[]} */
|
|
24
|
+
#timers = [];
|
|
25
|
+
|
|
26
|
+
/**@type {number} */
|
|
27
|
+
#counter;
|
|
28
|
+
|
|
29
|
+
/**@type {number}*/
|
|
30
|
+
#delay;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
*
|
|
34
|
+
* @param {number} size Semaphore counter initial value
|
|
35
|
+
* @param {number} delay
|
|
36
|
+
*/
|
|
37
|
+
constructor(size, delay){
|
|
38
|
+
this.#counter = size;
|
|
39
|
+
this.#delay = delay;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#startTimeout(){
|
|
43
|
+
let t = setTimeout(() => {
|
|
44
|
+
this.#release();
|
|
45
|
+
}, this.#delay);
|
|
46
|
+
this.#timers.push(t);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {Client} client
|
|
51
|
+
* @param {string} schema
|
|
52
|
+
* @param {{[varName: string]: value}} params
|
|
53
|
+
* @returns
|
|
54
|
+
*/
|
|
55
|
+
#execute(client, schema, params){
|
|
56
|
+
//console.log("Executing", params);
|
|
57
|
+
|
|
58
|
+
this.#startTimeout();
|
|
59
|
+
|
|
60
|
+
return client.request(schema, params);
|
|
61
|
+
|
|
62
|
+
/*return new Promise((resolve) => setTimeout(() => {
|
|
63
|
+
resolve(schema);
|
|
64
|
+
}, 500))*/
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {Query} query
|
|
69
|
+
* @returns
|
|
70
|
+
*/
|
|
71
|
+
#executeQuery(query){
|
|
72
|
+
return this.#execute(query.client, query.schema, query.params);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Executes the given query with the given GraphQL Client
|
|
77
|
+
* @param {Client} client
|
|
78
|
+
* @param {string} schema
|
|
79
|
+
* @param {{[varName: string]: value}} params
|
|
80
|
+
* @returns
|
|
81
|
+
*/
|
|
82
|
+
execute(client, schema, params){
|
|
83
|
+
if (this.#counter > 0){
|
|
84
|
+
this.#counter--;
|
|
85
|
+
//console.log("A ticket was available !", params);
|
|
86
|
+
return this.#execute(client, schema, params);
|
|
87
|
+
} else {
|
|
88
|
+
//console.log("No ticket vailable. Queueing.", params)
|
|
89
|
+
return new Promise((resolve, reject) => this.#queue.push({client, schema, params, resolve, reject}));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#release(){
|
|
94
|
+
//console.error("Release");
|
|
95
|
+
let query = this.#queue.shift();
|
|
96
|
+
if (query){
|
|
97
|
+
//console.error("A query was queued. Executing :", query.params);
|
|
98
|
+
this.#executeQuery(query)
|
|
99
|
+
.then(res => query.resolve(res))
|
|
100
|
+
.catch(err => query.reject(err));
|
|
101
|
+
} else {
|
|
102
|
+
this.#counter++;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
stop(){
|
|
107
|
+
for (let timer of this.#timers){
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export class ClockQueryLimiter extends TimedQuerySemaphore {
|
|
114
|
+
/** @param {number} rpm */
|
|
115
|
+
constructor(rpm){
|
|
116
|
+
super(1, 60000 / rpm);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export class StartGGClockQueryLimiter extends ClockQueryLimiter {
|
|
121
|
+
constructor(){
|
|
122
|
+
super(60);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export class DelayQueryLimiter extends TimedQuerySemaphore {
|
|
127
|
+
/** @param {number} rpm */
|
|
128
|
+
constructor(rpm){
|
|
129
|
+
super (rpm, 60000);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export class StartGGDelayQueryLimiter extends DelayQueryLimiter {
|
|
134
|
+
constructor(){
|
|
135
|
+
super(60)
|
|
136
|
+
}
|
|
137
|
+
}
|