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 ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./src/query.js"
2
+ export * from "./src/queryLimiter.js"
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
+ }