@vbarbarosh/node-helpers 1.1.1 → 1.1.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/README.md +1 -1
- package/package.json +7 -3
- package/src/array_gcd.js +16 -0
- package/src/array_gcd.test.js +10 -0
- package/src/array_index.js +8 -0
- package/src/array_sort.js +23 -0
- package/src/array_sort.test.js +10 -0
- package/src/array_sort_other.js +33 -0
- package/src/array_sort_other.test.js +9 -0
- package/src/array_sum.js +10 -0
- package/src/array_sum.test.js +11 -0
- package/src/cli.js +38 -0
- package/src/crypto_decrypt_aes256.js +16 -0
- package/src/crypto_encrypt_aes256.js +18 -0
- package/src/crypto_encrypt_aes256.test.js +10 -0
- package/src/crypto_hash_md5.js +14 -0
- package/src/crypto_hash_sha256.js +14 -0
- package/src/crypto_hash_sha256.test.js +10 -0
- package/src/crypto_hmac_sha256.js +15 -0
- package/src/format_thousands.js +7 -0
- package/src/fs_append.js +5 -0
- package/src/fs_append_json.js +9 -0
- package/src/fs_assert_file_exists.js +11 -0
- package/src/fs_assert_file_readable.js +14 -0
- package/src/fs_copy.js +5 -0
- package/src/fs_exists.js +12 -0
- package/src/fs_fclose.js +5 -0
- package/src/fs_fi.js +55 -0
- package/src/fs_find.js +21 -0
- package/src/fs_fopen.js +5 -0
- package/src/fs_fopen_update.js +34 -0
- package/src/fs_fread.js +15 -0
- package/src/fs_fsize.js +8 -0
- package/src/fs_fstat.js +5 -0
- package/src/fs_fwrite.js +15 -0
- package/src/fs_ls.js +16 -0
- package/src/fs_lsr.js +26 -0
- package/src/fs_lstat.js +5 -0
- package/src/fs_mkdir.js +5 -0
- package/src/fs_mkdirp.js +26 -0
- package/src/fs_path_basename.js +8 -0
- package/src/fs_path_dirname.js +8 -0
- package/src/fs_path_join.js +8 -0
- package/src/fs_path_resolve.js +8 -0
- package/src/fs_path_sanitize.js +8 -0
- package/src/fs_path_tempdir.js +8 -0
- package/src/fs_read.js +5 -0
- package/src/fs_read_json.js +10 -0
- package/src/fs_read_stream.js +1 -0
- package/src/fs_read_utf8.js +8 -0
- package/src/fs_readdir.js +5 -0
- package/src/fs_readlink.js +5 -0
- package/src/fs_rename.js +5 -0
- package/src/fs_rm.js +5 -0
- package/src/fs_rmdir.js +5 -0
- package/src/fs_rmrf.js +25 -0
- package/src/fs_size.js +8 -0
- package/src/fs_tempdir.js +42 -0
- package/src/fs_write.js +5 -0
- package/src/fs_write_stream.js +1 -0
- package/src/http_put_file.js +60 -0
- package/src/identity.js +6 -0
- package/src/ignore.js +5 -0
- package/src/json_stringify_stable.js +3 -0
- package/src/stream_data_ln.js +42 -0
- package/src/stream_data_ln.test.js +15 -0
- package/src/stream_promise.js +62 -0
- package/src/stream_promise.test.js +14 -0
- package/src/array_sort_by.js +0 -19
- package/src/array_sort_by.test.js +0 -9
- package/src/array_sort_by_keys.js +0 -33
- package/src/array_sort_by_keys.test.js +0 -9
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"author": "Vladimir Barbarosh <vladimir.barbarosh@gmail.com>",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"name": "@vbarbarosh/node-helpers",
|
|
5
|
-
"version": "1.1.
|
|
5
|
+
"version": "1.1.2",
|
|
6
6
|
"description": "A set of helpers for JavaScript/Node.js",
|
|
7
7
|
"files": [
|
|
8
8
|
"src"
|
|
@@ -24,12 +24,16 @@
|
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"axios": "^0.21.1",
|
|
27
|
-
"
|
|
27
|
+
"bluebird": "^3.7.2",
|
|
28
|
+
"form-data": "^4.0.0",
|
|
29
|
+
"json-stable-stringify": "^1.0.1",
|
|
30
|
+
"sanitize-filename": "^1.6.3"
|
|
28
31
|
},
|
|
29
32
|
"devDependencies": {
|
|
30
33
|
"@babel/preset-env": "^7.14.8",
|
|
31
34
|
"@babel/register": "^7.14.5",
|
|
32
35
|
"babel": "^6.23.0",
|
|
33
|
-
"mocha": "^9.0.2"
|
|
36
|
+
"mocha": "^9.0.2",
|
|
37
|
+
"throttle": "^1.0.3"
|
|
34
38
|
}
|
|
35
39
|
}
|
package/src/array_gcd.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// https://stackoverflow.com/a/39764792/1478566
|
|
2
|
+
function array_gcd(array, fn = Number)
|
|
3
|
+
{
|
|
4
|
+
return array.reduce((a,b) => gcd(fn(a), fn(b)));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// https://stackoverflow.com/a/39764792/1478566
|
|
8
|
+
function gcd(a, b)
|
|
9
|
+
{
|
|
10
|
+
if (b) {
|
|
11
|
+
return gcd(b, a % b);
|
|
12
|
+
}
|
|
13
|
+
return a;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default array_gcd;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import array_gcd from './array_gcd';
|
|
2
|
+
import assert from 'assert';
|
|
3
|
+
|
|
4
|
+
describe('array_gcd', function () {
|
|
5
|
+
it('should handle basic input', function () {
|
|
6
|
+
assert.deepStrictEqual(array_gcd([1,2,3,4,5]), 1);
|
|
7
|
+
assert.deepStrictEqual(array_gcd([4,8,16]), 4);
|
|
8
|
+
assert.deepStrictEqual(array_gcd([300,100,200,300]), 100);
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fcmp_strings from './fcmp_strings';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sort items in an `array`.
|
|
5
|
+
*
|
|
6
|
+
* @param array
|
|
7
|
+
* @param fn
|
|
8
|
+
* @param fcmp
|
|
9
|
+
* @returns {*}
|
|
10
|
+
*/
|
|
11
|
+
function array_sort(array, fn = identity, fcmp = fcmp_strings)
|
|
12
|
+
{
|
|
13
|
+
return array.sort(function (a, b) {
|
|
14
|
+
return fcmp(fn(a), fn(b));
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function identity(v)
|
|
19
|
+
{
|
|
20
|
+
return v;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default array_sort;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import array_sort from './array_sort';
|
|
2
|
+
import assert from 'assert';
|
|
3
|
+
|
|
4
|
+
describe('array_sort', function () {
|
|
5
|
+
it('should handle basic input', function () {
|
|
6
|
+
assert.deepStrictEqual(array_sort([5,4,3,2,1]), [1,2,3,4,5]);
|
|
7
|
+
assert.deepStrictEqual(array_sort([5,4,3,2,1], v => v), [1,2,3,4,5]);
|
|
8
|
+
assert.deepStrictEqual(array_sort([{id:5},{id:4},{id:3},{id:2},{id:1}], v => v.id), [{id:1},{id:2},{id:3},{id:4},{id:5}]);
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fcmp_strings from './fcmp_strings';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sort items in an `array` at the same order as in `other`. Values which
|
|
5
|
+
* are not in the `other` are added to the end of the result defined by `fcmp`.
|
|
6
|
+
*
|
|
7
|
+
* @param array
|
|
8
|
+
* @param fn
|
|
9
|
+
* @param other
|
|
10
|
+
* @param fcmp
|
|
11
|
+
* @returns {*}
|
|
12
|
+
*/
|
|
13
|
+
function array_sort_other(array, fn, other, fcmp = fcmp_strings)
|
|
14
|
+
{
|
|
15
|
+
const other_map = {};
|
|
16
|
+
other.forEach((v,i) => other_map[v] = i + 1);
|
|
17
|
+
return array.sort(function (a, b) {
|
|
18
|
+
const ax = other_map[fn(a)];
|
|
19
|
+
const bx = other_map[fn(b)];
|
|
20
|
+
if (ax && bx) {
|
|
21
|
+
return ax - bx;
|
|
22
|
+
}
|
|
23
|
+
if (ax) {
|
|
24
|
+
return -1;
|
|
25
|
+
}
|
|
26
|
+
if (bx) {
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
return fcmp(a, b);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default array_sort_other;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import array_sort_other from './array_sort_other';
|
|
2
|
+
import assert from 'assert';
|
|
3
|
+
|
|
4
|
+
describe('array_sort_other', function () {
|
|
5
|
+
it('should handle basic input', function () {
|
|
6
|
+
assert.deepStrictEqual(array_sort_other([1,2,3,4,5], v => v, [5]), [5,1,2,3,4]);
|
|
7
|
+
assert.deepStrictEqual(array_sort_other([{id:1},{id:2},{id:3},{id:4},{id:5}], v => v.id, [5]), [{id:5},{id:1},{id:2},{id:3},{id:4}]);
|
|
8
|
+
});
|
|
9
|
+
});
|
package/src/array_sum.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import array_sum from './array_sum';
|
|
2
|
+
import assert from 'assert';
|
|
3
|
+
|
|
4
|
+
describe('array_sum', function () {
|
|
5
|
+
it('should handle basic input', function () {
|
|
6
|
+
assert.deepStrictEqual(array_sum([1,2,3,4,5]), 15);
|
|
7
|
+
});
|
|
8
|
+
it('should cast strings to numbers', function () {
|
|
9
|
+
assert.deepStrictEqual(array_sum(['1','2','3','4','5']), 15);
|
|
10
|
+
});
|
|
11
|
+
});
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry point for Node CLI apps
|
|
3
|
+
*
|
|
4
|
+
* @link https://github.com/vbarbarosh/node-cli
|
|
5
|
+
*/
|
|
6
|
+
async function cli(main)
|
|
7
|
+
{
|
|
8
|
+
// https://stackoverflow.com/a/46916601/1478566
|
|
9
|
+
const timer = setInterval(v => v, 1E9);
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
return await main();
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
console.error(error);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
clearInterval(timer);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// import Promise from 'bluebird';
|
|
24
|
+
//
|
|
25
|
+
// // https://stackoverflow.com/a/57241059/1478566
|
|
26
|
+
// function cli(main)
|
|
27
|
+
// {
|
|
28
|
+
// // https://stackoverflow.com/a/46916601/1478566
|
|
29
|
+
// return Promise.method(main).call().catch(panic).finally(clearInterval.bind(null, setInterval(v=>v, 1E9)));
|
|
30
|
+
// }
|
|
31
|
+
//
|
|
32
|
+
// function panic(error)
|
|
33
|
+
// {
|
|
34
|
+
// console.error(error);
|
|
35
|
+
// process.exit(1);
|
|
36
|
+
// }
|
|
37
|
+
|
|
38
|
+
export default cli;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @link https://nodejs.org/api/crypto.html#crypto_class_cipher
|
|
5
|
+
* @link https://attacomsian.com/blog/nodejs-encrypt-decrypt-data
|
|
6
|
+
*/
|
|
7
|
+
function crypto_decrypt_aes256(password, ivenc)
|
|
8
|
+
{
|
|
9
|
+
const key = crypto.createHash('sha256').update(password).digest();
|
|
10
|
+
const iv = ivenc.slice(1, ivenc[0] + 1);
|
|
11
|
+
const enc = ivenc.slice(ivenc[0] + 1);
|
|
12
|
+
const decipher = crypto.createDecipheriv('aes-256-ctr', key, iv);
|
|
13
|
+
return Buffer.concat([decipher.update(enc), decipher.final()]);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default crypto_decrypt_aes256;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @link https://nodejs.org/api/crypto.html#crypto_class_cipher
|
|
5
|
+
* @link https://attacomsian.com/blog/nodejs-encrypt-decrypt-data
|
|
6
|
+
*/
|
|
7
|
+
function crypto_encrypt_aes256(password, data)
|
|
8
|
+
{
|
|
9
|
+
const iv = crypto.randomBytes(16);
|
|
10
|
+
const key = crypto.createHash('sha256').update(password).digest();
|
|
11
|
+
const cipher = crypto.createCipheriv('aes-256-ctr', key, iv);
|
|
12
|
+
if (iv.length > 255) {
|
|
13
|
+
throw new Error('iv size is too big');
|
|
14
|
+
}
|
|
15
|
+
return Buffer.concat([Buffer.from([iv.length]), iv, cipher.update(data), cipher.final()]);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default crypto_encrypt_aes256;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import crypto_decrypt_aes256 from './crypto_decrypt_aes256';
|
|
3
|
+
import crypto_encrypt_aes256 from './crypto_encrypt_aes256';
|
|
4
|
+
|
|
5
|
+
describe('crypto_encrypt_aes256', function () {
|
|
6
|
+
it('should handle basic input', function () {
|
|
7
|
+
const password = 'pass123';
|
|
8
|
+
assert.deepStrictEqual(crypto_decrypt_aes256(password, crypto_encrypt_aes256(password, 'hello')).toString('utf8'), 'hello');
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
*
|
|
5
|
+
* @param data
|
|
6
|
+
* @returns {Buffer}
|
|
7
|
+
* @link https://nodejs.org/en/knowledge/cryptography/how-to-use-crypto-module/#how-to-calculate-hashes-with-crypto
|
|
8
|
+
*/
|
|
9
|
+
function crypto_hash_md5(data)
|
|
10
|
+
{
|
|
11
|
+
return crypto.createHash('md5').update(data).digest();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default crypto_hash_md5;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
*
|
|
5
|
+
* @param data
|
|
6
|
+
* @returns {Buffer}
|
|
7
|
+
* @link https://nodejs.org/en/knowledge/cryptography/how-to-use-crypto-module/#how-to-calculate-hashes-with-crypto
|
|
8
|
+
*/
|
|
9
|
+
function crypto_hash_sha256(data)
|
|
10
|
+
{
|
|
11
|
+
return crypto.createHash('sha256').update(data).digest();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default crypto_hash_sha256;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import crypto_hash_sha256 from './crypto_hash_sha256';
|
|
2
|
+
import assert from 'assert';
|
|
3
|
+
|
|
4
|
+
describe('crypto_hash_sha256', function () {
|
|
5
|
+
it('should handle basic input', function () {
|
|
6
|
+
assert.deepStrictEqual(crypto_hash_sha256('').toString('hex'), 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855');
|
|
7
|
+
assert.deepStrictEqual(crypto_hash_sha256('1').toString('hex'), '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b');
|
|
8
|
+
assert.deepStrictEqual(crypto_hash_sha256('hello').toString('hex'), '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824');
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
*
|
|
5
|
+
* @param password
|
|
6
|
+
* @param data
|
|
7
|
+
* @returns {Buffer}
|
|
8
|
+
* @link https://nodejs.org/en/knowledge/cryptography/how-to-use-crypto-module/#hmac
|
|
9
|
+
*/
|
|
10
|
+
function crypto_hmac_sha256(password, data)
|
|
11
|
+
{
|
|
12
|
+
return crypto.createHmac('sha256', password).update(data).digest();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default crypto_hmac_sha256;
|
package/src/fs_append.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import fs_fi from './fs_fi';
|
|
2
|
+
|
|
3
|
+
async function fs_assert_file_readable(file)
|
|
4
|
+
{
|
|
5
|
+
const fi = await fs_fi(file);
|
|
6
|
+
if (!fi.isFile()) {
|
|
7
|
+
throw new Error(`Not a file: ${file}`);
|
|
8
|
+
}
|
|
9
|
+
if (fi.mode & 0o111) {
|
|
10
|
+
throw new Error(`File is not readable by all: ${file}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default fs_assert_file_readable;
|
package/src/fs_copy.js
ADDED
package/src/fs_exists.js
ADDED
package/src/fs_fclose.js
ADDED
package/src/fs_fi.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs_lstat from './fs_lstat';
|
|
2
|
+
import fs_path_basename from './fs_path_basename';
|
|
3
|
+
import fs_path_dirname from './fs_path_dirname';
|
|
4
|
+
import fs_path_resolve from './fs_path_resolve';
|
|
5
|
+
import fs_readlink from './fs_readlink';
|
|
6
|
+
|
|
7
|
+
async function fs_fi(pathname)
|
|
8
|
+
{
|
|
9
|
+
const basename = fs_path_basename(pathname);
|
|
10
|
+
const fi = await fs_lstat(pathname);
|
|
11
|
+
fi.pathname = pathname;
|
|
12
|
+
fi.basename = basename;
|
|
13
|
+
flags(fi);
|
|
14
|
+
if (fi.isSymbolicLink()) {
|
|
15
|
+
const target = await fs_readlink(fi.pathname);
|
|
16
|
+
const target_pathname = fs_path_resolve(fs_path_dirname(pathname), target);
|
|
17
|
+
fi.target = target;
|
|
18
|
+
fi.target_fi = await fs_lstat(target_pathname);
|
|
19
|
+
fi.target_fi.pathname = target_pathname;
|
|
20
|
+
fi.target_fi.basename = fs_path_basename(target_pathname);
|
|
21
|
+
flags(fi.target_fi);
|
|
22
|
+
}
|
|
23
|
+
return fi;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function flags(fi)
|
|
27
|
+
{
|
|
28
|
+
const tmp = [];
|
|
29
|
+
if (fi.isFile()) {
|
|
30
|
+
tmp.push('is_file');
|
|
31
|
+
}
|
|
32
|
+
if (fi.isDirectory()) {
|
|
33
|
+
tmp.push('is_directory');
|
|
34
|
+
}
|
|
35
|
+
if (fi.isBlockDevice()) {
|
|
36
|
+
tmp.push('is_block_device');
|
|
37
|
+
}
|
|
38
|
+
if (fi.isCharacterDevice()) {
|
|
39
|
+
tmp.push('is_character_device');
|
|
40
|
+
}
|
|
41
|
+
if (fi.isSymbolicLink()) {
|
|
42
|
+
tmp.push('is_symbolic_link');
|
|
43
|
+
}
|
|
44
|
+
if (fi.isFIFO()) {
|
|
45
|
+
tmp.push('is_fifo');
|
|
46
|
+
}
|
|
47
|
+
if (fi.isSocket()) {
|
|
48
|
+
tmp.push('is_socket');
|
|
49
|
+
}
|
|
50
|
+
fi.flags = tmp;
|
|
51
|
+
fi.flags_map = {};
|
|
52
|
+
tmp.forEach(key => fi.flags_map[key] = true);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default fs_fi;
|
package/src/fs_find.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Promise from 'bluebird';
|
|
2
|
+
import fs_fi from './fs_fi';
|
|
3
|
+
import fs_path_resolve from './fs_path_resolve';
|
|
4
|
+
import fs_readdir from './fs_readdir';
|
|
5
|
+
|
|
6
|
+
async function fs_find(pathname = '.')
|
|
7
|
+
{
|
|
8
|
+
const fi = await fs_fi(fs_path_resolve(pathname));
|
|
9
|
+
if (fi.isDirectory()) {
|
|
10
|
+
const names = await fs_readdir(pathname);
|
|
11
|
+
const rows = await Promise.all(names.map(v => fs_find(fs_path_resolve(pathname, v))));
|
|
12
|
+
const out = [fi];
|
|
13
|
+
for (let i = 0, end = rows.length; i < end; ++i) {
|
|
14
|
+
out.push(...rows[i]);
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
return [fi];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default fs_find;
|
package/src/fs_fopen.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs_fopen from './fs_fopen';
|
|
2
|
+
|
|
3
|
+
// Main use case is uploading files in chunks. Each time
|
|
4
|
+
// a chunk is uploaded do update a target file. At the end
|
|
5
|
+
// you will have a ready to go file.
|
|
6
|
+
|
|
7
|
+
async function fs_fopen_update(pathname)
|
|
8
|
+
{
|
|
9
|
+
try {
|
|
10
|
+
// Open a file for updating...
|
|
11
|
+
return await fs_fopen(pathname, 'r+');
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (error.code != 'ENOENT') {
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// There is no such a file, try to create it...
|
|
21
|
+
return await fs_fopen(pathname, 'wx');
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error.code != 'EEXIST') {
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Another process might have already created it, try to
|
|
30
|
+
// open for updating...
|
|
31
|
+
return fs_fopen(pathname, 'r+');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default fs_fopen_update;
|
package/src/fs_fread.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import util from 'util';
|
|
3
|
+
|
|
4
|
+
// https://nodejs.org/api/util.html#util_util_promisify_original
|
|
5
|
+
const read = util.promisify(fs.read);
|
|
6
|
+
|
|
7
|
+
// Main use case is for reading chunks of data from file.
|
|
8
|
+
|
|
9
|
+
async function fs_fread(fp, buffer, offset = null)
|
|
10
|
+
{
|
|
11
|
+
const {bytesRead} = await read(fp, buffer, 0, buffer.length, offset);
|
|
12
|
+
return (bytesRead == buffer.length) ? buffer : buffer.slice(0, bytesRead);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default fs_fread;
|
package/src/fs_fsize.js
ADDED
package/src/fs_fstat.js
ADDED
package/src/fs_fwrite.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import util from 'util';
|
|
3
|
+
|
|
4
|
+
// https://nodejs.org/api/util.html#util_util_promisify_original
|
|
5
|
+
const write = util.promisify(fs.write);
|
|
6
|
+
|
|
7
|
+
// Main use case is for writing chunks of data to file.
|
|
8
|
+
|
|
9
|
+
async function fs_fwrite(fp, buffer, offset = null)
|
|
10
|
+
{
|
|
11
|
+
const {bytesWritten} = await write(fp, buffer, 0, buffer.length, offset);
|
|
12
|
+
return bytesWritten;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default fs_fwrite;
|
package/src/fs_ls.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import Promise from 'bluebird';
|
|
2
|
+
import fs_fi from './fs_fi';
|
|
3
|
+
import fs_path_join from './fs_path_join';
|
|
4
|
+
import fs_readdir from './fs_readdir';
|
|
5
|
+
|
|
6
|
+
async function fs_ls(pathname = '.')
|
|
7
|
+
{
|
|
8
|
+
const fi = await fs_fi(pathname);
|
|
9
|
+
if (fi.isDirectory()) {
|
|
10
|
+
const names = await fs_readdir(pathname);
|
|
11
|
+
return Promise.all(names.map(v => fs_fi(fs_path_join(pathname, v))));
|
|
12
|
+
}
|
|
13
|
+
return [fi];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default fs_ls;
|
package/src/fs_lsr.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import Promise from 'bluebird';
|
|
2
|
+
import fs_fi from './fs_fi';
|
|
3
|
+
import fs_path_join from './fs_path_join';
|
|
4
|
+
import fs_readdir from './fs_readdir';
|
|
5
|
+
|
|
6
|
+
async function fs_lsr(pathname = '.')
|
|
7
|
+
{
|
|
8
|
+
const fi = await fs_fi(pathname);
|
|
9
|
+
const ret = [fi];
|
|
10
|
+
if (fi.isDirectory()) {
|
|
11
|
+
fi.content_size = 0;
|
|
12
|
+
const names = await fs_readdir(pathname);
|
|
13
|
+
const all = await Promise.all(names.map(v => fs_lsr(fs_path_join(pathname, v))));
|
|
14
|
+
for (let i = 0, end = all.length; i < end; ++i) {
|
|
15
|
+
const rows = all[i];
|
|
16
|
+
ret.push(...rows);
|
|
17
|
+
fi.content_size += rows[0].content_size;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
fi.content_size = fi.isFile() ? fi.size : 0;
|
|
22
|
+
}
|
|
23
|
+
return ret;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default fs_lsr;
|
package/src/fs_lstat.js
ADDED
package/src/fs_mkdir.js
ADDED
package/src/fs_mkdirp.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fs_lstat from './fs_lstat';
|
|
2
|
+
import fs_mkdir from './fs_mkdir';
|
|
3
|
+
import fs_path_dirname from './fs_path_dirname';
|
|
4
|
+
|
|
5
|
+
async function fs_mkdirp(pathname)
|
|
6
|
+
{
|
|
7
|
+
const parents = [];
|
|
8
|
+
|
|
9
|
+
for (let p = pathname; p && p != '/'; p = fs_path_dirname(p)) {
|
|
10
|
+
try {
|
|
11
|
+
await fs_lstat(p);
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
parents.unshift(p);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
for (let i = 0, end = parents.length; i < end; ++i) {
|
|
20
|
+
await fs_mkdir(parents[i]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return pathname;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default fs_mkdirp;
|
package/src/fs_read.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {createReadStream as default} from 'fs';
|
package/src/fs_rename.js
ADDED
package/src/fs_rm.js
ADDED
package/src/fs_rmdir.js
ADDED
package/src/fs_rmrf.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import Promise from 'bluebird';
|
|
2
|
+
import fs_fi from './fs_fi';
|
|
3
|
+
import fs_rm from './fs_rm';
|
|
4
|
+
import fs_rmdir from './fs_rmdir';
|
|
5
|
+
import fs_path_resolve from './fs_path_resolve';
|
|
6
|
+
import fs_readdir from './fs_readdir';
|
|
7
|
+
|
|
8
|
+
async function fs_rmrf(pathname)
|
|
9
|
+
{
|
|
10
|
+
const fi = await fs_fi(pathname).catch(() => null);
|
|
11
|
+
if (fi === null) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (fi.isDirectory()) {
|
|
16
|
+
const names = await fs_readdir(pathname);
|
|
17
|
+
await Promise.all(names.map(v => fs_rmrf(fs_path_resolve(pathname, v))));
|
|
18
|
+
await fs_rmdir(fi.pathname);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
await fs_rm(fi.pathname);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default fs_rmrf;
|
package/src/fs_size.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import fs_path_resolve from './fs_path_resolve';
|
|
3
|
+
import fs_path_tempdir from './fs_path_tempdir';
|
|
4
|
+
import fs_rmrf from './fs_rmrf';
|
|
5
|
+
import util from 'util';
|
|
6
|
+
|
|
7
|
+
// https://nodejs.org/api/util.html#util_util_promisify_original
|
|
8
|
+
const mkdtemp = util.promisify(fs.mkdtemp);
|
|
9
|
+
|
|
10
|
+
// * https://www.cons.org/cracauer/sigint.html from https://stackoverflow.com/q/14031763/1478566
|
|
11
|
+
// * http://www.tldp.org/LDP/abs/html/exitcodes.html from https://stackoverflow.com/q/14031763/1478566
|
|
12
|
+
// * https://www.exratione.com/2013/05/die-child-process-die/
|
|
13
|
+
|
|
14
|
+
async function fs_tempdir(fn)
|
|
15
|
+
{
|
|
16
|
+
process.on('SIGINT', sigint);
|
|
17
|
+
|
|
18
|
+
let d = null, out;
|
|
19
|
+
try {
|
|
20
|
+
d = await mkdtemp(fs_path_resolve(fs_path_tempdir(), 'vbtemp'));
|
|
21
|
+
out = await fn(d);
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
await clean();
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
|
|
28
|
+
async function clean() {
|
|
29
|
+
process.removeListener('SIGINT', sigint);
|
|
30
|
+
if (d) {
|
|
31
|
+
await fs_rmrf(d);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function sigint() {
|
|
36
|
+
await clean().catch(function () {/* ignore */});
|
|
37
|
+
// https://stackoverflow.com/questions/14031763/doing-a-cleanup-action-just-before-node-js-exits#comment68567869_21947851
|
|
38
|
+
process.kill(process.pid, 'SIGINT');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default fs_tempdir;
|
package/src/fs_write.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {createWriteStream as default} from 'fs';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import fs_size from './fs_size';
|
|
3
|
+
import http from 'http';
|
|
4
|
+
import https from 'https';
|
|
5
|
+
|
|
6
|
+
async function http_put_file(url, file, options)
|
|
7
|
+
{
|
|
8
|
+
// <?xml version="1.0" encoding="UTF-8"?>
|
|
9
|
+
// <Error>
|
|
10
|
+
// <Code>NotImplemented</Code>
|
|
11
|
+
// <Message>A header you provided implies functionality that is not implemented</Message>
|
|
12
|
+
// <Header>Transfer-Encoding</Header>
|
|
13
|
+
// <RequestId>XXXXXXXXXX</RequestId>
|
|
14
|
+
// <HostId>YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY</HostId>
|
|
15
|
+
// </Error>
|
|
16
|
+
|
|
17
|
+
const total = await fs_size(file);
|
|
18
|
+
return new Promise(function (resolve, reject) {
|
|
19
|
+
const chunks = [];
|
|
20
|
+
const request = (url.startsWith('http:') ? http : https).request(url, {method: 'PUT', headers: {'Content-Length': total}}, function (response) {
|
|
21
|
+
response.on('error', reject);
|
|
22
|
+
if (options.progress_download) {
|
|
23
|
+
const total_download = response.headers['content-length'] ? parseInt(response.headers['content-length']) : null;
|
|
24
|
+
let ready = 0;
|
|
25
|
+
response.on('data', function (chunk) {
|
|
26
|
+
const delta = chunk.length;
|
|
27
|
+
ready += delta;
|
|
28
|
+
options.progress_download(delta, ready, total_download);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
response.on('data', function (chunk) {
|
|
32
|
+
chunks.push(chunk);
|
|
33
|
+
});
|
|
34
|
+
response.on('end', function () {
|
|
35
|
+
switch (response.statusCode) {
|
|
36
|
+
case 200:
|
|
37
|
+
resolve({request, response, data: Buffer.concat(chunks)});
|
|
38
|
+
break;
|
|
39
|
+
default:
|
|
40
|
+
reject(new Error(`${response.statusCode} ${response.statusMessage}\n\n${Buffer.concat(chunks).toString('utf8')}`));
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
request.on('error', reject);
|
|
46
|
+
// https://stackoverflow.com/a/39492211/1478566
|
|
47
|
+
if (options.progress_upload) {
|
|
48
|
+
let prev = 0;
|
|
49
|
+
request.on('drain', function () {
|
|
50
|
+
const ready = request.socket.bytesWritten;
|
|
51
|
+
const delta = ready - prev;
|
|
52
|
+
options.progress_upload(delta, ready, total);
|
|
53
|
+
prev = ready;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
fs.createReadStream(file).pipe(request);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default http_put_file;
|
package/src/identity.js
ADDED
package/src/ignore.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split stream on each \n and call `fn` for each line.
|
|
3
|
+
*
|
|
4
|
+
* @param stream
|
|
5
|
+
* @param fn
|
|
6
|
+
* @return function
|
|
7
|
+
*/
|
|
8
|
+
function stream_data_ln(stream, fn)
|
|
9
|
+
{
|
|
10
|
+
let utf8 = '';
|
|
11
|
+
stream.on('data', data);
|
|
12
|
+
stream.on('end', end);
|
|
13
|
+
return off;
|
|
14
|
+
|
|
15
|
+
function off() {
|
|
16
|
+
stream.off('data', data);
|
|
17
|
+
stream.off('end', end);
|
|
18
|
+
}
|
|
19
|
+
function end() {
|
|
20
|
+
off();
|
|
21
|
+
if (utf8) {
|
|
22
|
+
fn(utf8, true);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function data(buffer) {
|
|
26
|
+
utf8 += buffer.toString('utf8');
|
|
27
|
+
for (let iteration = 1; true; ++iteration) {
|
|
28
|
+
if (iteration == 1000000) {
|
|
29
|
+
throw new Error('Too many iterations');
|
|
30
|
+
}
|
|
31
|
+
const i = utf8.indexOf('\n');
|
|
32
|
+
if (i == -1) {
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
const line = utf8.substr(0, i);
|
|
36
|
+
utf8 = utf8.substr(i + 1);
|
|
37
|
+
fn(line, false);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default stream_data_ln;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import stream_data_ln from './stream_data_ln';
|
|
4
|
+
import stream_promise from './stream_promise';
|
|
5
|
+
|
|
6
|
+
describe('stream_data_ln', function () {
|
|
7
|
+
it('should handle basic input', async function () {
|
|
8
|
+
let tmp = '';
|
|
9
|
+
const s = fs.createReadStream(__filename);
|
|
10
|
+
const off = stream_data_ln(s, v => tmp += v + '\n');
|
|
11
|
+
await stream_promise(s);
|
|
12
|
+
off();
|
|
13
|
+
assert.deepStrictEqual(tmp, fs.readFileSync(__filename, {encoding: 'utf8'}));
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {finished} from 'stream/promises';
|
|
2
|
+
|
|
3
|
+
// /**
|
|
4
|
+
// * Wait for a stream to finish with `await stream_promise(stream)`.
|
|
5
|
+
// *
|
|
6
|
+
// * @param stream
|
|
7
|
+
// * @returns {Promise}
|
|
8
|
+
// */
|
|
9
|
+
// function stream_promise(stream)
|
|
10
|
+
// {
|
|
11
|
+
// /**
|
|
12
|
+
// * https://stackoverflow.com/a/34310963/1478566
|
|
13
|
+
// * > end and finish are the same event BUT on different types of
|
|
14
|
+
// * > Streams.
|
|
15
|
+
// * > * stream.Readable fires ONLY end and NEVER finish
|
|
16
|
+
// * > * stream.Writable fires ONLY finish and NEVER end
|
|
17
|
+
// */
|
|
18
|
+
// return new Promise(function (resolve, reject) {
|
|
19
|
+
// stream.once('end', resolve); // Stream.Readable
|
|
20
|
+
// stream.once('finish', resolve); // Stream.Writable
|
|
21
|
+
// stream.once('error', reject);
|
|
22
|
+
// function end() { off(); resolve(); }
|
|
23
|
+
// function finish() { off(); resolve(); }
|
|
24
|
+
// function error() { off(); reject(); }
|
|
25
|
+
// function off() {
|
|
26
|
+
// stream.off('end', end);
|
|
27
|
+
// stream.off('finish', finish);
|
|
28
|
+
// stream.off('error', error);
|
|
29
|
+
// }
|
|
30
|
+
// });
|
|
31
|
+
// }
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Wait for a stream to finish with `await stream_promise(stream)`.
|
|
35
|
+
*
|
|
36
|
+
* https://nodejs.org/api/stream.html#stream_stream_finished_stream_options_callback
|
|
37
|
+
* > Especially useful in error handling scenarios where a stream
|
|
38
|
+
* > is destroyed prematurely (like an aborted HTTP request), and
|
|
39
|
+
* > will not emit 'end' or 'finish'.
|
|
40
|
+
*
|
|
41
|
+
* @param stream
|
|
42
|
+
* @returns {Promise<void>}
|
|
43
|
+
*/
|
|
44
|
+
function stream_promise(stream)
|
|
45
|
+
{
|
|
46
|
+
// https://nodejs.org/api/stream.html#stream_stream_finished_stream_options_callback
|
|
47
|
+
// > stream.finished() leaves dangling event listeners (in particular
|
|
48
|
+
// > 'error', 'end', 'finish' and 'close') after callback has been
|
|
49
|
+
// > invoked. The reason for this is so that unexpected 'error'
|
|
50
|
+
// > events (due to incorrect stream implementations) do not cause
|
|
51
|
+
// > unexpected crashes. If this is unwanted behavior then the
|
|
52
|
+
// > returned cleanup function needs to be invoked in the callback:
|
|
53
|
+
// >
|
|
54
|
+
// > const cleanup = finished(rs, (err) => {
|
|
55
|
+
// > cleanup();
|
|
56
|
+
// > // ...
|
|
57
|
+
// > });
|
|
58
|
+
// >
|
|
59
|
+
return finished(stream);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default stream_promise;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import ignore from './ignore';
|
|
3
|
+
import stream_promise from './stream_promise';
|
|
4
|
+
|
|
5
|
+
describe('stream_promise', function () {
|
|
6
|
+
it('should handle ReadableStream', async function () {
|
|
7
|
+
this.timeout(100);
|
|
8
|
+
await stream_promise(fs.createReadStream(__filename).on('data', ignore));
|
|
9
|
+
});
|
|
10
|
+
it('should handle WritableStream', async function () {
|
|
11
|
+
this.timeout(100);
|
|
12
|
+
await stream_promise(fs.createWriteStream('/dev/null').end('hello\n'));
|
|
13
|
+
});
|
|
14
|
+
});
|
package/src/array_sort_by.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import fcmp_strings from './fcmp_strings';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Sort items in an `array` at the same order as in `other`. Values which
|
|
5
|
-
* are not in `other` are added to the end in the order defined by `fcmp`.
|
|
6
|
-
*
|
|
7
|
-
* @param array
|
|
8
|
-
* @param key_from_item
|
|
9
|
-
* @param fcmp
|
|
10
|
-
* @returns {*}
|
|
11
|
-
*/
|
|
12
|
-
function array_sort_by(array, key_from_item, fcmp = fcmp_strings)
|
|
13
|
-
{
|
|
14
|
-
return array.sort(function (a, b) {
|
|
15
|
-
return fcmp(key_from_item(a), key_from_item(b));
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export default array_sort_by;
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import array_sort_by from './array_sort_by';
|
|
2
|
-
import assert from 'assert';
|
|
3
|
-
|
|
4
|
-
describe('array_sort_by', function () {
|
|
5
|
-
it('should handle basic input', function () {
|
|
6
|
-
assert.deepStrictEqual(array_sort_by([5,4,3,2,1], v => v), [1,2,3,4,5]);
|
|
7
|
-
assert.deepStrictEqual(array_sort_by([{id:5},{id:4},{id:3},{id:2},{id:1}], v => v.id), [{id:1},{id:2},{id:3},{id:4},{id:5}]);
|
|
8
|
-
});
|
|
9
|
-
});
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import fcmp_strings from './fcmp_strings';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Sort items in an `array` at the same order as in `keys`. Values which
|
|
5
|
-
* are not in the `keys` are added to the end of the result defined by `fcmp`.
|
|
6
|
-
*
|
|
7
|
-
* @param array
|
|
8
|
-
* @param key_from_item
|
|
9
|
-
* @param keys
|
|
10
|
-
* @param fcmp
|
|
11
|
-
* @returns {*}
|
|
12
|
-
*/
|
|
13
|
-
function array_sort_by_keys(array, key_from_item, keys, fcmp = fcmp_strings)
|
|
14
|
-
{
|
|
15
|
-
const other_map = {};
|
|
16
|
-
keys.forEach((v,i) => other_map[v] = i + 1);
|
|
17
|
-
return array.sort(function (a, b) {
|
|
18
|
-
const ax = other_map[key_from_item(a)];
|
|
19
|
-
const bx = other_map[key_from_item(b)];
|
|
20
|
-
if (ax && bx) {
|
|
21
|
-
return ax - bx;
|
|
22
|
-
}
|
|
23
|
-
if (ax) {
|
|
24
|
-
return -1;
|
|
25
|
-
}
|
|
26
|
-
if (bx) {
|
|
27
|
-
return 1;
|
|
28
|
-
}
|
|
29
|
-
return fcmp(a, b);
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export default array_sort_by_keys;
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import array_sort_by_keys from './array_sort_by_keys';
|
|
2
|
-
import assert from 'assert';
|
|
3
|
-
|
|
4
|
-
describe('array_sort_by_keys', function () {
|
|
5
|
-
it('should handle basic input', function () {
|
|
6
|
-
assert.deepStrictEqual(array_sort_by_keys([1,2,3,4,5], v => v, [5]), [5,1,2,3,4]);
|
|
7
|
-
assert.deepStrictEqual(array_sort_by_keys([{id:1},{id:2},{id:3},{id:4},{id:5}], v => v.id, [5]), [{id:5},{id:1},{id:2},{id:3},{id:4}]);
|
|
8
|
-
});
|
|
9
|
-
});
|