mm_mysql 2.2.5 → 2.2.6
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 +3 -4
- package/README_EN.md +478 -0
- package/db.js +497 -496
- package/eslint.config.js +235 -0
- package/index.js +601 -601
- package/package.json +12 -5
- package/sql.js +1390 -1276
- package/test.js +481 -485
package/README.md
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# mm_mysql
|
|
2
|
-
一个简洁、高效的Node.js MySQL操作库,支持async/await语法,提供直观的API和强大的功能扩展。
|
|
3
2
|
|
|
4
|
-
[
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
[中文](./README.md) | [English](./README_EN.md)
|
|
4
|
+
|
|
5
|
+
一个简洁、高效的Node.js MySQL操作库,支持async/await语法,提供直观的API和强大的功能扩展。
|
|
7
6
|
|
|
8
7
|
## 安装
|
|
9
8
|
|
package/README_EN.md
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
# mm_mysql
|
|
2
|
+
|
|
3
|
+
[中文](./README.md) | [English](./README_EN.md)
|
|
4
|
+
|
|
5
|
+
A concise and efficient Node.js MySQL operation library with async/await support, providing intuitive API and powerful feature extensions.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install mm_mysql
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Basic Usage
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
const { Mysql } = require('mm_mysql');
|
|
17
|
+
|
|
18
|
+
// Create mysql instance
|
|
19
|
+
const mysql = new Mysql({
|
|
20
|
+
host: "localhost",
|
|
21
|
+
port: 3306,
|
|
22
|
+
user: "root",
|
|
23
|
+
password: "your_password",
|
|
24
|
+
database: "your_database"
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Open connection (no need to call init() method)
|
|
28
|
+
await mysql.open();
|
|
29
|
+
|
|
30
|
+
// Get database manager and set table name
|
|
31
|
+
const db = mysql.db().new('users', 'id');
|
|
32
|
+
|
|
33
|
+
// Perform operations...
|
|
34
|
+
|
|
35
|
+
// Close connection when done
|
|
36
|
+
await mysql.close();
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Key Features
|
|
40
|
+
|
|
41
|
+
- Support for async/await syntax, avoiding callback hell
|
|
42
|
+
- Clean and intuitive API interface
|
|
43
|
+
- Built-in transaction support
|
|
44
|
+
- Automatic type inference for table creation (supports int, float, varchar, text, datetime, date, time, boolean, and more)
|
|
45
|
+
- Flexible query condition support
|
|
46
|
+
- Connection pool management support
|
|
47
|
+
- Debug mode support
|
|
48
|
+
|
|
49
|
+
## API Documentation
|
|
50
|
+
|
|
51
|
+
### Mysql Class
|
|
52
|
+
|
|
53
|
+
#### Constructor
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
new Mysql(config)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Parameter description:
|
|
60
|
+
- `config` {Object} Database configuration object
|
|
61
|
+
|
|
62
|
+
#### Configuration Parameters
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
const config = {
|
|
66
|
+
host: "127.0.0.1", // Server address
|
|
67
|
+
port: 3306, // Port number
|
|
68
|
+
user: "root", // Connection username
|
|
69
|
+
password: "password", // Connection password
|
|
70
|
+
database: "dbname", // Database name
|
|
71
|
+
charset: "utf8mb4", // Character set
|
|
72
|
+
timezone: "+08:00", // Timezone
|
|
73
|
+
connect_timeout: 20000, // Connection timeout (milliseconds)
|
|
74
|
+
connection_limit: 10, // Connection pool size
|
|
75
|
+
acquire_timeout: 20000, // Connection acquisition timeout (milliseconds)
|
|
76
|
+
wait_for_connections: true // Whether to wait for connections
|
|
77
|
+
};
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
#### Main Methods
|
|
81
|
+
|
|
82
|
+
##### open()
|
|
83
|
+
Open database connection
|
|
84
|
+
- Returns: {Promise<boolean>} Connection result
|
|
85
|
+
|
|
86
|
+
##### close()
|
|
87
|
+
Close database connection
|
|
88
|
+
- Returns: {Promise<boolean>} Close result
|
|
89
|
+
|
|
90
|
+
##### run(sql, params)
|
|
91
|
+
Execute query SQL statement
|
|
92
|
+
- `sql` {String} SQL statement
|
|
93
|
+
- `params` {Array} Parameter array (optional)
|
|
94
|
+
- Returns: {Promise<Array>} Query result array
|
|
95
|
+
|
|
96
|
+
##### exec(sql, params)
|
|
97
|
+
Execute non-query SQL statement (insert, update, delete)
|
|
98
|
+
- `sql` {String} SQL statement
|
|
99
|
+
- `params` {Array} Parameter array (optional)
|
|
100
|
+
- Returns: {Promise<number>} Number of affected rows
|
|
101
|
+
|
|
102
|
+
##### db()
|
|
103
|
+
Get database manager instance
|
|
104
|
+
- Returns: {Object} DB class instance
|
|
105
|
+
|
|
106
|
+
### DB Class
|
|
107
|
+
|
|
108
|
+
#### Constructor
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
new DB(mysql, table, key)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Parameter description:
|
|
115
|
+
- `mysql` {Object} Mysql instance
|
|
116
|
+
- `table` {String} Table name
|
|
117
|
+
- `key` {String} Primary key field name
|
|
118
|
+
|
|
119
|
+
#### Main Methods
|
|
120
|
+
|
|
121
|
+
##### new(table, key)
|
|
122
|
+
Create new DB instance with specified table and key
|
|
123
|
+
- `table` {String} Table name
|
|
124
|
+
- `key` {String} Primary key field name
|
|
125
|
+
- Returns: {Object} New DB instance
|
|
126
|
+
|
|
127
|
+
##### table(table)
|
|
128
|
+
Set table name
|
|
129
|
+
- `table` {String} Table name
|
|
130
|
+
- Returns: {Object} Current DB instance
|
|
131
|
+
|
|
132
|
+
##### key(key)
|
|
133
|
+
Set primary key
|
|
134
|
+
- `key` {String} Primary key field name
|
|
135
|
+
- Returns: {Object} Current DB instance
|
|
136
|
+
|
|
137
|
+
##### add(data, timeout)
|
|
138
|
+
Insert data
|
|
139
|
+
- `data` {Object} Data to insert
|
|
140
|
+
- `timeout` {Number} Timeout in milliseconds (optional, default: 15000)
|
|
141
|
+
- Returns: {Promise<Object>} Insert result
|
|
142
|
+
|
|
143
|
+
##### set(where, data, timeout)
|
|
144
|
+
Update data
|
|
145
|
+
- `where` {Object} Where conditions
|
|
146
|
+
- `data` {Object} Data to update
|
|
147
|
+
- `timeout` {Number} Timeout in milliseconds (optional, default: 15000)
|
|
148
|
+
- Returns: {Promise<number>} Number of affected rows
|
|
149
|
+
|
|
150
|
+
##### del(where, timeout)
|
|
151
|
+
Delete data
|
|
152
|
+
- `where` {Object} Where conditions
|
|
153
|
+
- `timeout` {Number} Timeout in milliseconds (optional, default: 15000)
|
|
154
|
+
- Returns: {Promise<number>} Number of affected rows
|
|
155
|
+
|
|
156
|
+
##### get(where, options, timeout)
|
|
157
|
+
Query data
|
|
158
|
+
- `where` {Object} Where conditions (optional)
|
|
159
|
+
- `options` {Object} Query options (optional)
|
|
160
|
+
- `timeout` {Number} Timeout in milliseconds (optional, default: 15000)
|
|
161
|
+
- Returns: {Promise<Array>} Query result array
|
|
162
|
+
|
|
163
|
+
##### count(where, timeout)
|
|
164
|
+
Count records
|
|
165
|
+
- `where` {Object} Where conditions (optional)
|
|
166
|
+
- `timeout` {Number} Timeout in milliseconds (optional, default: 15000)
|
|
167
|
+
- Returns: {Promise<number>} Record count
|
|
168
|
+
|
|
169
|
+
##### sum(field, where, timeout)
|
|
170
|
+
Sum field values
|
|
171
|
+
- `field` {String} Field name
|
|
172
|
+
- `where` {Object} Where conditions (optional)
|
|
173
|
+
- `timeout` {Number} Timeout in milliseconds (optional, default: 15000)
|
|
174
|
+
- Returns: {Promise<number>} Sum result
|
|
175
|
+
|
|
176
|
+
##### max(field, where, timeout)
|
|
177
|
+
Get maximum value
|
|
178
|
+
- `field` {String} Field name
|
|
179
|
+
- `where` {Object} Where conditions (optional)
|
|
180
|
+
- `timeout` {Number} Timeout in milliseconds (optional, default: 15000)
|
|
181
|
+
- Returns: {Promise<number>} Maximum value
|
|
182
|
+
|
|
183
|
+
##### min(field, where, timeout)
|
|
184
|
+
Get minimum value
|
|
185
|
+
- `field` {String} Field name
|
|
186
|
+
- `where` {Object} Where conditions (optional)
|
|
187
|
+
- `timeout` {Number} Timeout in milliseconds (optional, default: 15000)
|
|
188
|
+
- Returns: {Promise<number>} Minimum value
|
|
189
|
+
|
|
190
|
+
#### Table Operations
|
|
191
|
+
|
|
192
|
+
##### addTable(table, field, type, auto, commit, timeout)
|
|
193
|
+
Create table
|
|
194
|
+
- `table` {String} Table name
|
|
195
|
+
- `field` {String} Field name
|
|
196
|
+
- `type` {String} Field type
|
|
197
|
+
- `auto` {Boolean} Auto increment (optional)
|
|
198
|
+
- `commit` {Boolean} Commit transaction (optional)
|
|
199
|
+
- `timeout` {Number} Timeout in milliseconds (optional, default: 15000)
|
|
200
|
+
- Returns: {Promise<boolean>} Create result
|
|
201
|
+
|
|
202
|
+
##### dropTable(table, timeout)
|
|
203
|
+
Drop table
|
|
204
|
+
- `table` {String} Table name
|
|
205
|
+
- `timeout` {Number} Timeout in milliseconds (optional, default: 15000)
|
|
206
|
+
- Returns: {Promise<boolean>} Drop result
|
|
207
|
+
|
|
208
|
+
##### renameTable(table, new_table, timeout)
|
|
209
|
+
Rename table
|
|
210
|
+
- `table` {String} Original table name
|
|
211
|
+
- `new_table` {String} New table name
|
|
212
|
+
- `timeout` {Number} Timeout in milliseconds (optional, default: 15000)
|
|
213
|
+
- Returns: {Promise<boolean>} Rename result
|
|
214
|
+
|
|
215
|
+
##### emptyTable(table, timeout)
|
|
216
|
+
Empty table
|
|
217
|
+
- `table` {String} Table name
|
|
218
|
+
- `timeout` {Number} Timeout in milliseconds (optional, default: 15000)
|
|
219
|
+
- Returns: {Promise<boolean>} Empty result
|
|
220
|
+
|
|
221
|
+
##### hasTable(table, timeout)
|
|
222
|
+
Check if table exists
|
|
223
|
+
- `table` {String} Table name
|
|
224
|
+
- `timeout` {Number} Timeout in milliseconds (optional, default: 15000)
|
|
225
|
+
- Returns: {Promise<boolean>} Existence result
|
|
226
|
+
|
|
227
|
+
#### Field Operations
|
|
228
|
+
|
|
229
|
+
##### addField(table, field, type, len, def, not_null, auto, comment, timeout)
|
|
230
|
+
Add field
|
|
231
|
+
- `table` {String} Table name
|
|
232
|
+
- `field` {String} Field name
|
|
233
|
+
- `type` {String} Field type
|
|
234
|
+
- `len` {Number} Field length (optional)
|
|
235
|
+
- `def` {String} Default value (optional)
|
|
236
|
+
- `not_null` {Boolean} Not null constraint (optional)
|
|
237
|
+
- `auto` {Boolean} Auto increment (optional)
|
|
238
|
+
- `comment` {String} Field comment (optional)
|
|
239
|
+
- `timeout` {Number} Timeout in milliseconds (optional, default: 15000)
|
|
240
|
+
- Returns: {Promise<boolean>} Add result
|
|
241
|
+
|
|
242
|
+
##### dropField(table, field, timeout)
|
|
243
|
+
Drop field
|
|
244
|
+
- `table` {String} Table name
|
|
245
|
+
- `field` {String} Field name
|
|
246
|
+
- `timeout` {Number} Timeout in milliseconds (optional, default: 15000)
|
|
247
|
+
- Returns: {Promise<boolean>} Drop result
|
|
248
|
+
|
|
249
|
+
##### renameField(table, field, new_field, type, timeout)
|
|
250
|
+
Rename field
|
|
251
|
+
- `table` {String} Table name
|
|
252
|
+
- `field` {String} Original field name
|
|
253
|
+
- `new_field` {String} New field name
|
|
254
|
+
- `type` {String} Field type
|
|
255
|
+
- `timeout` {Number} Timeout in milliseconds (optional, default: 15000)
|
|
256
|
+
- Returns: {Promise<boolean>} Rename result
|
|
257
|
+
|
|
258
|
+
##### fields(table, field_name, timeout)
|
|
259
|
+
Get table fields
|
|
260
|
+
- `table` {String} Table name
|
|
261
|
+
- `field_name` {String} Specific field name (optional)
|
|
262
|
+
- `timeout` {Number} Timeout in milliseconds (optional, default: 15000)
|
|
263
|
+
- Returns: {Promise<Array>} Field information array
|
|
264
|
+
|
|
265
|
+
##### hasField(table, field, timeout)
|
|
266
|
+
Check if field exists
|
|
267
|
+
- `table` {String} Table name
|
|
268
|
+
- `field` {String} Field name
|
|
269
|
+
- `timeout` {Number} Timeout in milliseconds (optional, default: 15000)
|
|
270
|
+
- Returns: {Promise<boolean>} Existence result
|
|
271
|
+
|
|
272
|
+
#### Transaction Operations
|
|
273
|
+
|
|
274
|
+
##### start()
|
|
275
|
+
Start transaction
|
|
276
|
+
- Returns: {Promise<Object>} Transaction object
|
|
277
|
+
|
|
278
|
+
##### commit(transaction)
|
|
279
|
+
Commit transaction
|
|
280
|
+
- `transaction` {Object} Transaction object
|
|
281
|
+
- Returns: {Promise<void>}
|
|
282
|
+
|
|
283
|
+
##### rollback(transaction)
|
|
284
|
+
Rollback transaction
|
|
285
|
+
- `transaction` {Object} Transaction object
|
|
286
|
+
- Returns: {Promise<void>}
|
|
287
|
+
|
|
288
|
+
#### Utility Methods
|
|
289
|
+
|
|
290
|
+
##### size(size)
|
|
291
|
+
Set batch size
|
|
292
|
+
- `size` {Number} Batch size
|
|
293
|
+
- Returns: {Object} Current DB instance
|
|
294
|
+
|
|
295
|
+
##### getTableData(table, batchSize, timeout)
|
|
296
|
+
Get table data in batches
|
|
297
|
+
- `table` {String} Table name
|
|
298
|
+
- `batchSize` {Number} Batch size (optional, default: 100)
|
|
299
|
+
- `timeout` {Number} Timeout in milliseconds (optional, default: 60000)
|
|
300
|
+
- Returns: {Promise<Array>} Table data array
|
|
301
|
+
|
|
302
|
+
## Query Conditions
|
|
303
|
+
|
|
304
|
+
### Comparison Operators
|
|
305
|
+
- `_eq` - Equal to
|
|
306
|
+
- `_neq` - Not equal to
|
|
307
|
+
- `_gt` - Greater than
|
|
308
|
+
- `_gte` - Greater than or equal to
|
|
309
|
+
- `_lt` - Less than
|
|
310
|
+
- `_lte` - Less than or equal to
|
|
311
|
+
|
|
312
|
+
### Logical Operators
|
|
313
|
+
- `_and` - Logical AND
|
|
314
|
+
- `_or` - Logical OR
|
|
315
|
+
- `_not` - Logical NOT
|
|
316
|
+
|
|
317
|
+
### Array Operators
|
|
318
|
+
- `_in` - In array
|
|
319
|
+
- `_nin` - Not in array
|
|
320
|
+
|
|
321
|
+
### String Operators
|
|
322
|
+
- `_like` - Like pattern
|
|
323
|
+
- `_nlike` - Not like pattern
|
|
324
|
+
|
|
325
|
+
### Null Operators
|
|
326
|
+
- `_null` - Is null
|
|
327
|
+
- `_nnull` - Is not null
|
|
328
|
+
|
|
329
|
+
## Query Options
|
|
330
|
+
|
|
331
|
+
### Pagination
|
|
332
|
+
- `limit` - Number of records to return
|
|
333
|
+
- `offset` - Number of records to skip
|
|
334
|
+
|
|
335
|
+
### Sorting
|
|
336
|
+
- `order` - Sort order (e.g., 'name asc', 'created_at desc')
|
|
337
|
+
|
|
338
|
+
### Field Selection
|
|
339
|
+
- `field` - Specific fields to return
|
|
340
|
+
- `group` - Group by fields
|
|
341
|
+
|
|
342
|
+
## Advanced Usage Examples
|
|
343
|
+
|
|
344
|
+
### Complex Queries
|
|
345
|
+
|
|
346
|
+
```javascript
|
|
347
|
+
// Complex conditional query with sorting and pagination
|
|
348
|
+
const users = await db.get({
|
|
349
|
+
status: 'active',
|
|
350
|
+
age: { _gte: 18, _lte: 65 },
|
|
351
|
+
email: { _like: '%@example.com' }
|
|
352
|
+
}, {
|
|
353
|
+
order: 'created_at desc',
|
|
354
|
+
limit: 10,
|
|
355
|
+
offset: 0,
|
|
356
|
+
field: 'id,username,email'
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Count with conditions
|
|
360
|
+
const activeUserCount = await db.count({
|
|
361
|
+
status: 'active',
|
|
362
|
+
created_at: { _gte: new Date('2023-01-01') }
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Aggregate functions
|
|
366
|
+
const totalAge = await db.sum('age', { status: 'active' });
|
|
367
|
+
const oldestAge = await db.max('age', { status: 'active' });
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Transaction Management
|
|
371
|
+
|
|
372
|
+
```javascript
|
|
373
|
+
// Transaction example
|
|
374
|
+
const transaction = await db.start();
|
|
375
|
+
try {
|
|
376
|
+
// Insert multiple records
|
|
377
|
+
await db.add({ username: 'Alice', email: 'alice@example.com' });
|
|
378
|
+
await db.add({ username: 'Bob', email: 'bob@example.com' });
|
|
379
|
+
|
|
380
|
+
// Update record
|
|
381
|
+
await db.set({ username: 'Alice' }, { status: 'verified' });
|
|
382
|
+
|
|
383
|
+
// Commit transaction
|
|
384
|
+
await db.commit(transaction);
|
|
385
|
+
console.log('Transaction completed successfully');
|
|
386
|
+
} catch (error) {
|
|
387
|
+
// Rollback on error
|
|
388
|
+
await db.rollback(transaction);
|
|
389
|
+
console.error('Transaction failed:', error.message);
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Table and Field Operations
|
|
394
|
+
|
|
395
|
+
```javascript
|
|
396
|
+
// Create table
|
|
397
|
+
await db.addTable('users', 'id', 'int', true);
|
|
398
|
+
|
|
399
|
+
// Add fields
|
|
400
|
+
await db.addField('users', 'username', 'varchar', 50, '', true);
|
|
401
|
+
await db.addField('users', 'email', 'varchar', 100, '', true);
|
|
402
|
+
await db.addField('users', 'age', 'int', null, '0', false);
|
|
403
|
+
|
|
404
|
+
// Check if table exists
|
|
405
|
+
const tableExists = await db.hasTable('users');
|
|
406
|
+
|
|
407
|
+
// Get table fields
|
|
408
|
+
const fields = await db.fields('users');
|
|
409
|
+
|
|
410
|
+
// Check if field exists
|
|
411
|
+
const fieldExists = await db.hasField('users', 'email');
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## Error Handling
|
|
415
|
+
|
|
416
|
+
All methods include comprehensive error handling:
|
|
417
|
+
|
|
418
|
+
```javascript
|
|
419
|
+
try {
|
|
420
|
+
const result = await db.get({ id: 1 });
|
|
421
|
+
console.log('Query result:', result);
|
|
422
|
+
} catch (error) {
|
|
423
|
+
console.error('Query failed:', error.message);
|
|
424
|
+
// Handle specific error types
|
|
425
|
+
if (error.code === 'ER_NO_SUCH_TABLE') {
|
|
426
|
+
console.error('Table does not exist');
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
## Performance Tips
|
|
432
|
+
|
|
433
|
+
1. **Use Connection Pooling**: Set `connection_limit` > 1 for better concurrent performance
|
|
434
|
+
2. **Batch Operations**: Use transactions for multiple related operations
|
|
435
|
+
3. **Query Optimization**: Use appropriate indexes and limit result sets
|
|
436
|
+
4. **Connection Management**: Close connections when not in use
|
|
437
|
+
|
|
438
|
+
## Development Specifications
|
|
439
|
+
|
|
440
|
+
### Code Style
|
|
441
|
+
- Use 2-space indentation
|
|
442
|
+
- Single quotes for strings
|
|
443
|
+
- One class per file or multiple functions
|
|
444
|
+
|
|
445
|
+
### Naming Conventions
|
|
446
|
+
- Class names: PascalCase
|
|
447
|
+
- Function/method names: camelCase
|
|
448
|
+
- Parameters/variables: snake_case
|
|
449
|
+
- Constants: UPPER_SNAKE_CASE
|
|
450
|
+
- Maximum name length: 20 characters
|
|
451
|
+
- Prefer single-word names
|
|
452
|
+
|
|
453
|
+
### Error Handling
|
|
454
|
+
- Parameter validation using `throw new TypeError()`
|
|
455
|
+
- Use try...catch for calling other class methods
|
|
456
|
+
- Chinese error messages (for Chinese version)
|
|
457
|
+
|
|
458
|
+
### Performance
|
|
459
|
+
- Method length ≤ 40 lines
|
|
460
|
+
- Single line ≤ 100 characters
|
|
461
|
+
- Each class focuses on single responsibility
|
|
462
|
+
- Each method does one thing only
|
|
463
|
+
|
|
464
|
+
## License
|
|
465
|
+
|
|
466
|
+
MIT License - see LICENSE file for details.
|
|
467
|
+
|
|
468
|
+
## Contributing
|
|
469
|
+
|
|
470
|
+
1. Fork the repository
|
|
471
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
472
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
473
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
474
|
+
5. Open a Pull Request
|
|
475
|
+
|
|
476
|
+
## Support
|
|
477
|
+
|
|
478
|
+
For issues and questions, please open an issue on the GitHub repository.
|