From 32b1c78c4e4ccc89dffb287576e6dce375c98f8e Mon Sep 17 00:00:00 2001 From: "dr.dimitru" Date: Sun, 5 Jan 2025 19:16:44 +0200 Subject: [PATCH 1/4] package.json by @bratelefant --- package.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 package.json diff --git a/package.json b/package.json new file mode 100644 index 0000000..16d31d6 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "scripts": { + "test:tiny": "meteor test-packages ./", + "test:mocha": "meteor test-packages ./ --driver-package meteortesting:mocha --once", + "test:mocha:watch": "meteor test-packages ./ --driver-package meteortesting:mocha", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "dependencies": { + "chai": "^4.3.10", + "sinon": "^17.0.1" + }, + "devDependencies": { + "@babel/core": "^7.23.6", + "@babel/eslint-parser": "^7.23.3", + "eslint": "^8.56.0" + } +} From 8ebba339ede72d45c6efc39a02ad5380584f0dcc Mon Sep 17 00:00:00 2001 From: "dr.dimitru" Date: Sun, 5 Jan 2025 19:17:52 +0200 Subject: [PATCH 2/4] package.json: bump dependencies --- package-lock.json | 2057 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 8 +- 2 files changed, 2061 insertions(+), 4 deletions(-) create mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ded1be3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2057 @@ +{ + "name": "meteor-files", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "chai": "^4.5.0", + "sinon": "^17.0.1" + }, + "devDependencies": { + "@babel/core": "^7.26.0", + "@babel/eslint-parser": "^7.25.9", + "eslint": "^8.57.1" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz", + "integrity": "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", + "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.0", + "@babel/generator": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.0", + "@babel/parser": "^7.26.0", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.26.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/eslint-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.25.9.tgz", + "integrity": "sha512-5UXfgpK0j0Xr/xIdgdLEhOFxaDZ0bRPWJJchRpqOSur/3rZoPbqqki5mm0p4NE2cs28krBEiSM2MB7//afRSQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || >=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0", + "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", + "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.3", + "@babel/types": "^7.26.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", + "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", + "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", + "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", + "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.3", + "@babel/parser": "^7.26.3", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.3", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", + "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-scope": "5.1.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/commons/node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.3.1.tgz", + "integrity": "sha512-EVJO7nW5M/F5Tur0Rf2z/QoMo+1Ia963RiMtapiQrEWvY0iBUvADo8Beegwjpnle5BHkyHuoxSTW3jF43H1XRA==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", + "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "lodash.get": "^4.4.2", + "type-detect": "^4.1.0" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "license": "(Unlicense OR Apache-2.0)" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.1.tgz", + "integrity": "sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", + "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001690", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", + "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.76", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.76.tgz", + "integrity": "sha512-CjVQyG7n7Sr+eBXE86HIulnL5N8xZY1sgmOPGuq/F0Rr0FJq63lg0kEtOIDfZBk44FnDLf6FUJ+dsJcuiUDdDQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", + "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nise": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", + "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index 16d31d6..1afcf0f 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,12 @@ "lint:fix": "eslint . --fix" }, "dependencies": { - "chai": "^4.3.10", + "chai": "^4.5.0", "sinon": "^17.0.1" }, "devDependencies": { - "@babel/core": "^7.23.6", - "@babel/eslint-parser": "^7.23.3", - "eslint": "^8.56.0" + "@babel/core": "^7.26.0", + "@babel/eslint-parser": "^7.25.9", + "eslint": "^8.57.1" } } From 978e31a97fb5acac73687da71e65a7184474fa82 Mon Sep 17 00:00:00 2001 From: "dr.dimitru" Date: Sun, 5 Jan 2025 19:18:18 +0200 Subject: [PATCH 3/4] workflows by @bratelefant --- .github/workflows/lint.yml | 27 +++++++++++++++++++++++++++ .github/workflows/testsuite.yml | 29 +++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/testsuite.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..86732b1 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,27 @@ +name: Lint + +on: [push, pull_request] + +jobs: + lintcode: + name: lint + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v3 + + - name: setup node + uses: actions/setup-node@v3 + with: + node-version: '14.x' + + - name: cache dependencies + uses: actions/cache@v3 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - run: npm install + - run: npm run lint \ No newline at end of file diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml new file mode 100644 index 0000000..f868376 --- /dev/null +++ b/.github/workflows/testsuite.yml @@ -0,0 +1,29 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: Test suite + +on: [push, pull_request] + +jobs: + tests: + name: tests + runs-on: ubuntu-latest + # needs: [lintcode,lintstyle,lintdocs] # we could add prior jobs for linting, if desired + steps: + - name: checkout + uses: actions/checkout@v3 + + - name: Setup meteor + uses: meteorengineer/setup-meteor@v1 + with: + meteor-release: '2.14' + + - name: cache dependencies + uses: actions/cache@v1 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + - run: meteor npm install && meteor npm run test:mocha \ No newline at end of file From 86b00dce1ce36660ca59d6665489bd12894d7098 Mon Sep 17 00:00:00 2001 From: "dr.dimitru" Date: Sun, 19 Jan 2025 12:35:50 +0200 Subject: [PATCH 4/4] all changes by bratelefant for comparison with migration branch --- client.js | 8 +- core.js | 21 +- cursor.js | 129 ++++---- lib.js | 2 + package.js | 11 +- server.js | 711 ++++++++++++++++++++++--------------------- tests/core.test.js | 172 +++++++++++ tests/cursor.test.js | 319 +++++++++++++++++++ tests/server.js | 4 + tests/server.test.js | 597 ++++++++++++++++++++++++++++++++++++ upload.js | 93 +++--- 11 files changed, 1596 insertions(+), 471 deletions(-) create mode 100644 tests/core.test.js create mode 100644 tests/cursor.test.js create mode 100644 tests/server.js create mode 100644 tests/server.test.js diff --git a/client.js b/client.js index a5f0f46..9f17df4 100644 --- a/client.js +++ b/client.js @@ -255,7 +255,13 @@ class FilesCollection extends FilesCollectionCore { Meteor._debug('[FilesCollection] [insert()] Upload is disabled with [disableUpload]!'); return {}; } - return (new UploadInstance(config, this))[autoStart ? 'start' : 'manual'](); + const uploadInstance = new UploadInstance(config, this); + if (autoStart) { + uploadInstance.start().catch((error) => { + console.error('[FilesCollection] [insert] Error starting upload:', error); + }); + } + return uploadInstance; } /** diff --git a/core.js b/core.js index dd803d3..181d3fb 100644 --- a/core.js +++ b/core.js @@ -105,7 +105,8 @@ export default class FilesCollectionCore extends EventEmitter { */ _debug() { if (this.debug) { - (console.info || console.log || function () { }).apply(void 0, arguments); + // eslint-disable-next-line no-console + (console.info || console.log || function () {}).apply(void 0, arguments); } } @@ -203,18 +204,18 @@ export default class FilesCollectionCore extends EventEmitter { /* * @locus Anywhere * @memberOf FilesCollectionCore - * @name findOne + * @name findOneAsync * @param {String|Object} selector - Mongo-Style selector (http://docs.meteor.com/api/collections.html#selectors) * @param {Object} options - Mongo-Style selector Options (http://docs.meteor.com/api/collections.html#sortspecifiers) * @summary Find and return Cursor for matching document Object - * @returns {FileCursor} Instance + * @returns {Promise} Instance */ - findOne(selector = {}, options) { - this._debug(`[FilesCollection] [findOne(${JSON.stringify(selector)}, ${JSON.stringify(options)})]`); + async findOneAsync(selector = {}, options) { + this._debug(`[FilesCollection] [findOneAsync(${JSON.stringify(selector)}, ${JSON.stringify(options)})]`); check(selector, Match.Optional(Match.OneOf(Object, String, Boolean, Number, null))); check(options, Match.Optional(Object)); - const doc = this.collection.findOne(selector, options); + const doc = await this.collection.findOneAsync(selector, options); if (doc) { return new FileCursor(doc, this); } @@ -241,13 +242,13 @@ export default class FilesCollectionCore extends EventEmitter { /* * @locus Anywhere * @memberOf FilesCollectionCore - * @name update + * @name updateAsync * @see http://docs.meteor.com/#/full/update * @summary link Mongo.Collection update method - * @returns {Mongo.Collection} Instance + * @returns {Promise} Instance */ - update() { - this.collection.update.apply(this.collection, arguments); + async updateAsync() { + await this.collection.updateAsync.apply(this.collection, arguments); return this.collection; } diff --git a/cursor.js b/cursor.js index d7583db..1424d11 100644 --- a/cursor.js +++ b/cursor.js @@ -18,17 +18,17 @@ export class FileCursor { /* * @locus Anywhere * @memberOf FileCursor - * @name remove - * @param callback {Function} - Triggered asynchronously after item is removed or failed to be removed + * @name removeAsync + * @throws {Meteor.Error} - If no file reference is provided * @summary Remove document - * @returns {FileCursor} + * @returns {Promise} */ - remove(callback) { - this._collection._debug('[FilesCollection] [FileCursor] [remove()]'); + async removeAsync() { + this._collection._debug('[FilesCollection] [FileCursor] [removeAsync()]'); if (this._fileRef) { - this._collection.remove(this._fileRef._id, callback); + await this._collection.removeAsync(this._fileRef._id); } else { - callback && callback(new Meteor.Error(404, 'No such file')); + throw new Meteor.Error(404, 'No such file'); } return this; } @@ -113,11 +113,11 @@ export class FilesCursor { * @memberOf FilesCursor * @name get * @summary Returns all matching document(s) as an Array. Alias of `.fetch()` - * @returns {[Object]} + * @returns {Promise<[Object]>} */ - get() { - this._collection._debug('[FilesCollection] [FilesCursor] [get()]'); - return this.cursor.fetch(); + async get() { + this._collection._debug('[FilesCollection] [FilesCursor] [getAsync()]'); + return this.cursor.fetchAsync(); } /* @@ -127,9 +127,9 @@ export class FilesCursor { * @summary Returns `true` if there is next item available on Cursor * @returns {Boolean} */ - hasNext() { - this._collection._debug('[FilesCollection] [FilesCursor] [hasNext()]'); - return this._current < (this.cursor.count() - 1); + async hasNext() { + this._collection._debug('[FilesCollection] [FilesCursor] [hasNextAsync()]'); + return this._current < (await this.cursor.countAsync()) - 1; } /* @@ -137,11 +137,11 @@ export class FilesCursor { * @memberOf FilesCursor * @name next * @summary Returns next item on Cursor, if available - * @returns {Object|undefined} + * @returns {Promise} */ - next() { - this._collection._debug('[FilesCollection] [FilesCursor] [next()]'); - this.cursor.fetch()[++this._current]; + async next() { + this._collection._debug('[FilesCollection] [FilesCursor] [nextAsync()]'); + return (await this.cursor.fetchAsync())[++this._current]; } /* @@ -161,23 +161,23 @@ export class FilesCursor { * @memberOf FilesCursor * @name previous * @summary Returns previous item on Cursor, if available - * @returns {Object|undefined} + * @returns {Promise} */ - previous() { - this._collection._debug('[FilesCollection] [FilesCursor] [previous()]'); - this.cursor.fetch()[--this._current]; + async previous() { + this._collection._debug('[FilesCollection] [FilesCursor] [previousAsync()]'); + return (this.cursor.fetchAsync())[--this._current]; } /* * @locus Anywhere * @memberOf FilesCursor - * @name fetch + * @name fetchAsync * @summary Returns all matching document(s) as an Array. - * @returns {[Object]} + * @returns {Promise<[Object]>} */ - fetch() { - this._collection._debug('[FilesCollection] [FilesCursor] [fetch()]'); - return this.cursor.fetch() || []; + async fetchAsync() { + this._collection._debug('[FilesCollection] [FilesCursor] [fetchAsync()]'); + return (await this.cursor.fetchAsync()) || []; } /* @@ -185,12 +185,12 @@ export class FilesCursor { * @memberOf FilesCursor * @name first * @summary Returns first item on Cursor, if available - * @returns {Object|undefined} + * @returns {Promise} */ - first() { - this._collection._debug('[FilesCollection] [FilesCursor] [first()]'); + async first() { + this._collection._debug('[FilesCollection] [FilesCursor] [firstAsync()]'); this._current = 0; - return this.fetch()[this._current]; + return (await this.fetchAsync())[this._current]; } /* @@ -198,52 +198,51 @@ export class FilesCursor { * @memberOf FilesCursor * @name last * @summary Returns last item on Cursor, if available - * @returns {Object|undefined} + * @returns {Promise} */ - last() { + async last() { this._collection._debug('[FilesCollection] [FilesCursor] [last()]'); - this._current = this.count() - 1; - return this.fetch()[this._current]; + this._current = (await this.countAsync()) - 1; + return (await this.fetchAsync())[this._current]; } /* * @locus Anywhere * @memberOf FilesCursor - * @name count + * @name countAsync * @summary Returns the number of documents that match a query - * @returns {Number} + * @returns {Promise} */ - count() { - this._collection._debug('[FilesCollection] [FilesCursor] [count()]'); - return this.cursor.count(); + async countAsync() { + this._collection._debug('[FilesCollection] [FilesCursor] [countAsync()]'); + return this.cursor.countAsync(); } /* * @locus Anywhere * @memberOf FilesCursor - * @name remove - * @param callback {Function} - Triggered asynchronously after item is removed or failed to be removed + * @name removeAsync * @summary Removes all documents that match a query - * @returns {FilesCursor} + * @returns {Promise} */ - remove(callback) { - this._collection._debug('[FilesCollection] [FilesCursor] [remove()]'); - this._collection.remove(this._selector, callback); + async removeAsync() { + this._collection._debug('[FilesCollection] [FilesCursor] [removeAsync()]'); + await this._collection.removeAsync(this._selector); return this; } /* * @locus Anywhere * @memberOf FilesCursor - * @name forEach + * @name forEachAsync * @param callback {Function} - Function to call. It will be called with three arguments: the `file`, a 0-based index, and cursor itself * @param context {Object} - An object which will be the value of `this` inside `callback` * @summary Call `callback` once for each matching document, sequentially and synchronously. - * @returns {undefined} + * @returns {Promise} */ - forEach(callback, context = {}) { - this._collection._debug('[FilesCollection] [FilesCursor] [forEach()]'); - this.cursor.forEach(callback, context); + async forEachAsync(callback, context = {}) { + this._collection._debug('[FilesCollection] [FilesCursor] [forEachAsync()]'); + await this.cursor.forEachAsync(callback, context); } /* @@ -252,10 +251,10 @@ export class FilesCursor { * @name each * @summary Returns an Array of FileCursor made for each document on current cursor * Useful when using in {{#each FilesCursor#each}}...{{/each}} block template helper - * @returns {[FileCursor]} + * @returns {Promise<[FileCursor]>} */ - each() { - return this.map((file) => { + async each() { + return this.mapAsync((file) => { return new FileCursor(file, this._collection); }); } @@ -263,15 +262,15 @@ export class FilesCursor { /* * @locus Anywhere * @memberOf FilesCursor - * @name map + * @name mapAsync * @param callback {Function} - Function to call. It will be called with three arguments: the `file`, a 0-based index, and cursor itself * @param context {Object} - An object which will be the value of `this` inside `callback` * @summary Map `callback` over all matching documents. Returns an Array. - * @returns {Array} + * @returns {Promise} */ - map(callback, context = {}) { - this._collection._debug('[FilesCollection] [FilesCursor] [map()]'); - return this.cursor.map(callback, context); + async mapAsync(callback, context = {}) { + this._collection._debug('[FilesCollection] [FilesCursor] [mapAsync()]'); + return this.cursor.mapAsync(callback, context); } /* @@ -279,14 +278,14 @@ export class FilesCursor { * @memberOf FilesCursor * @name current * @summary Returns current item on Cursor, if available - * @returns {Object|undefined} + * @returns {Promise} */ - current() { - this._collection._debug('[FilesCollection] [FilesCursor] [current()]'); + async current() { + this._collection._debug('[FilesCollection] [FilesCursor] [currentAsync()]'); if (this._current < 0) { this._current = 0; } - return this.fetch()[this._current]; + return (await this.fetchAsync())[this._current]; } /* @@ -296,9 +295,9 @@ export class FilesCursor { * @param callbacks {Object} - Functions to call to deliver the result set as it changes * @summary Watch a query. Receive callbacks as the result set changes. * @url http://docs.meteor.com/api/collections.html#Mongo-Cursor-observe - * @returns {Object} - live query handle + * @returns {Promise} - live query handle */ - observe(callbacks) { + async observe(callbacks) { this._collection._debug('[FilesCollection] [FilesCursor] [observe()]'); return this.cursor.observe(callbacks); } diff --git a/lib.js b/lib.js index 3d0431e..72de7a4 100644 --- a/lib.js +++ b/lib.js @@ -184,12 +184,14 @@ const fixJSONStringify = function(obj) { * @summary Returns formatted URL for file * @returns {String} Downloadable link */ +// eslint-disable-next-line camelcase, no-undef const formatFleURL = (fileRef, version = 'original', _uriBase = (__meteor_runtime_config__ || {}).ROOT_URL) => { check(fileRef, Object); check(version, String); let uriBase = _uriBase; if (!helpers.isString(uriBase)) { + // eslint-disable-next-line camelcase, no-undef uriBase = (__meteor_runtime_config__ || {}).ROOT_URL || '/'; } diff --git a/package.js b/package.js index b8914f6..7dd72e9 100755 --- a/package.js +++ b/package.js @@ -1,16 +1,16 @@ Package.describe({ name: 'ostrio:files', - version: '2.3.3', + version: '3.0.0', summary: 'Upload files to a server or 3rd party storage: AWS:S3, GridFS, DropBox, and other', git: 'https://github.com/veliovgroup/Meteor-Files', documentation: 'README.md' }); Package.onUse((api) => { - api.versionsFrom('1.9'); + api.versionsFrom(['3.0']); api.use('webapp', 'server'); api.use(['reactive-var', 'tracker', 'ddp-client'], 'client'); - api.use(['mongo', 'check', 'random', 'ecmascript', 'fetch', 'ostrio:cookies@2.7.2'], ['client', 'server']); + api.use(['mongo', 'check', 'random', 'ecmascript', 'fetch', 'ostrio:cookies@2.9.0'], ['client', 'server']); api.addAssets('worker.min.js', 'client'); api.mainModule('server.js', 'server'); api.mainModule('client.js', 'client'); @@ -19,11 +19,14 @@ Package.onUse((api) => { Package.onTest((api) => { api.use('tinytest'); + api.use('meteortesting:mocha'); api.use(['ecmascript', 'ostrio:files'], ['client', 'server']); api.addFiles('tests/helpers.js', ['client', 'server']); + api.mainModule('tests/server.js', 'server'); }); Npm.depends({ - eventemitter3: '4.0.7', + 'fs-extra': '11.2.0', + eventemitter3: '5.0.1', 'abort-controller': '3.0.0' }); diff --git a/server.js b/server.js index 2038a69..fc0e3a0 100644 --- a/server.js +++ b/server.js @@ -14,6 +14,13 @@ import AbortController from 'abort-controller'; import fs from 'fs'; import nodeQs from 'querystring'; import nodePath from 'path'; +// in Node.js 14, there is no promises version of stream +import { pipeline as pipelineCallback } from 'stream'; +import { promisify } from 'util'; +// change to this in "loadAsync" when Meteor supports Node.js 15 upwards +//import nodeStream from 'stream/promises'; + +const pipeline = promisify(pipelineCallback); /** * @const {Object} bound - Meteor.bindEnvironment (Fiber wrapper) @@ -22,6 +29,7 @@ import nodePath from 'path'; const bound = Meteor.bindEnvironment(callback => callback()); const noop = function noop () {}; + /** * Create (ensure) index on MongoDB collection, catch and log exception if thrown * @function createIndex @@ -100,14 +108,14 @@ const createIndex = async (_collection, keys, opts) => { * @param config.onAfterRemove {Function} - [Server] Called right after file is removed. Removed objects is passed to callback * @param config.continueUploadTTL {Number} - [Server] Time in seconds, during upload may be continued, default 3 hours (10800 seconds) * @param config.onBeforeUpload {Function}- [Both] Function which executes on server after receiving each chunk and on client right before beginning upload. Function context is `File` - so you are able to check for extension, mime-type, size and etc.: - * - return `true` to continue - * - return `false` or `String` to abort upload + * - return or resolve `true` to continue + * - return or resolve `false` or `String` to abort upload * @param config.getUser {Function} - [Server] Replace default way of recognizing user, usefull when you want to auth user based on custom cookie (or other way). arguments {http: {request: {...}, response: {...}}}, need to return {userId: String, user: Function} * @param config.onInitiateUpload {Function} - [Server] Function which executes on server right before upload is begin and right after `onBeforeUpload` hook. This hook is fully asynchronous. * @param config.onBeforeRemove {Function} - [Server] Executes before removing file on server, so you can check permissions. Return `true` to allow action and `false` to deny. * @param config.allowClientCode {Boolean} - [Both] Allow to run `remove` from client * @param config.downloadCallback {Function} - [Server] Callback triggered each time file is requested, return truthy value to continue download, or falsy to abort - * @param config.interceptRequest {Function} - [Server] Intercept incoming HTTP request, so you can whatever you want, no checks or preprocessing, arguments {http: {request: {...}, response: {...}}, params: {...}} + * @param config.interceptRequest {Function} - [Server] Intercept incoming HTTP request, so you can whatever you want, no checks or preprocessing, arguments {http: {request: {...}, response: {...}}, params: {...}} * @param config.interceptDownload {Function} - [Server] Intercept download request, so you can serve file from third-party resource, arguments {http: {request: {...}, response: {...}}, fileRef: {...}} * @param config.disableUpload {Boolean} - Disable file upload, useful for server only solutions * @param config.disableDownload {Boolean} - Disable file download (serving), useful for file management only solutions @@ -401,13 +409,13 @@ class FilesCollection extends FilesCollectionCore { }); _preCollectionCursor.observe({ - changed(doc) { + async changed(doc) { if (doc.isFinished) { self._debug(`[FilesCollection] [_preCollectionCursor.observe] [changed]: ${doc._id}`); - self._preCollection.remove({_id: doc._id}, noop); + await self._preCollection.removeAsync({_id: doc._id}); } }, - removed(doc) { + async removed(doc) { // Free memory after upload is done // Or if upload is unfinished self._debug(`[FilesCollection] [_preCollectionCursor.observe] [removed]: ${doc._id}`); @@ -417,7 +425,7 @@ class FilesCollection extends FilesCollectionCore { // We can be unlucky to run into a race condition where another server removed this document before the change of `isFinished` is registered on this server. // Therefore it's better to double-check with the main collection if the file is referenced there. Issue: https://github.com/veliovgroup/Meteor-Files/issues/672 - if (!doc.isFinished && self.collection.find({ _id: doc._id }).count() === 0) { + if (!doc.isFinished && (await self.collection.find({ _id: doc._id }).countAsync()) === 0) { self._debug(`[FilesCollection] [_preCollectionCursor.observe] [removeUnfinishedUpload]: ${doc._id}`); self._currentUploads[doc._id].abort(); } @@ -433,7 +441,7 @@ class FilesCollection extends FilesCollectionCore { // This little function allows to continue upload // even after server is restarted (*not on dev-stage*) - this._continueUpload = (_id) => { + this._continueUpload = async (_id) => { if (this._currentUploads[_id] && this._currentUploads[_id].file) { if (!this._currentUploads[_id].aborted && !this._currentUploads[_id].ended) { return this._currentUploads[_id].file; @@ -441,7 +449,7 @@ class FilesCollection extends FilesCollectionCore { this._createStream(_id, this._currentUploads[_id].file.file.path, this._currentUploads[_id].file); return this._currentUploads[_id].file; } - const contUpld = this._preCollection.findOne({_id}); + const contUpld = await this._preCollection.findOneAsync({_id}); if (contUpld) { this._createStream(_id, contUpld.file.path, contUpld); return this._currentUploads[_id].file; @@ -470,18 +478,18 @@ class FilesCollection extends FilesCollectionCore { throw new Meteor.Error(500, `[FilesCollection.${this.collectionName}]: Files can not be public and protected at the same time!`); } - this._checkAccess = (http) => { + this._checkAccess = async (http) => { if (this.protected) { let result; - const {user, userId} = this._getUser(http); + const {userAsync, userId} = this._getUser(http); if (helpers.isFunction(this.protected)) { let fileRef; if (helpers.isObject(http.params) && http.params._id) { - fileRef = this.collection.findOne(http.params._id); + fileRef = await this.collection.findOneAsync(http.params._id); } - result = http ? this.protected.call(Object.assign(http, {user, userId}), (fileRef || null)) : this.protected.call({user, userId}, (fileRef || null)); + result = http ? await this.protected.call(Object.assign(http, {userAsync, userId}), (fileRef || null)) : await this.protected.call({userAsync, userId}, (fileRef || null)); } else { result = !!userId; } @@ -525,7 +533,7 @@ class FilesCollection extends FilesCollectionCore { if (this.disableUpload && this.disableDownload) { return; } - WebApp.connectHandlers.use((httpReq, httpResp, next) => { + WebApp.connectHandlers.use(async (httpReq, httpResp, next) => { if (this.allowedOrigins && httpReq._parsedUrl.path.includes(`${this.downloadRoute}/`) && !httpResp.headersSent) { if (this.allowedOrigins.test(httpReq.headers.origin)) { httpResp.setHeader('Access-Control-Allow-Credentials', 'true'); @@ -571,7 +579,7 @@ class FilesCollection extends FilesCollectionCore { }; let body = ''; - const handleData = () => { + const handleData = async () => { try { let opts; let result; @@ -590,12 +598,12 @@ class FilesCollection extends FilesCollectionCore { opts.chunkId = parseInt(httpReq.headers['x-chunkid']); } - const _continueUpload = this._continueUpload(opts.fileId); + const _continueUpload = await this._continueUpload(opts.fileId); if (!_continueUpload) { throw new Meteor.Error(408, 'Can\'t continue upload, session expired. Start upload again.'); } - ({result, opts} = this._prepareUpload(Object.assign(opts, _continueUpload), user.userId, 'HTTP')); + ({result, opts} = await this._prepareUpload(Object.assign(opts, _continueUpload), user.userId, 'HTTP')); if (opts.eof) { // FINISH UPLOAD SCENARIO: @@ -665,16 +673,20 @@ class FilesCollection extends FilesCollectionCore { } opts.___s = true; - ({result} = this._prepareUpload(helpers.clone(opts), user.userId, 'HTTP Start Method')); + ({result} = await this._prepareUpload(helpers.clone(opts), user.userId, 'HTTP Start Method')); + + let res; + res = await this.collection.findOneAsync(result._id); - if (this.collection.findOne(result._id)) { + if (res) { throw new Meteor.Error(400, 'Can\'t start upload, data substitution detected!'); } opts._id = opts.fileId; opts.createdAt = new Date(); opts.maxLength = opts.fileLength; - this._preCollection.insert(helpers.omit(opts, '___s')); + + await this._preCollection.insertAsync(helpers.omit(opts, '___s')); this._createStream(result._id, result.path, helpers.omit(opts, '___s')); if (opts.returnMeta) { @@ -739,12 +751,13 @@ class FilesCollection extends FilesCollectionCore { }; const http = {request: httpReq, response: httpResp, params}; - if (this.interceptRequest && helpers.isFunction(this.interceptRequest) && this.interceptRequest(http) === true) { + + if (this.interceptRequest && helpers.isFunction(this.interceptRequest) && (await this.interceptRequest(http)) === true) { return; } - if (this._checkAccess(http)) { - this.download(http, uris[1], this.collection.findOne(uris[0])); + if (await this._checkAccess(http)) { + await this.download(http, uris[1], await this.collection.findOneAsync(uris[0])); } } else { next(); @@ -779,10 +792,10 @@ class FilesCollection extends FilesCollectionCore { name: _file }; const http = {request: httpReq, response: httpResp, params}; - if (this.interceptRequest && helpers.isFunction(this.interceptRequest) && this.interceptRequest(http) === true) { + if (this.interceptRequest && helpers.isFunction(this.interceptRequest) && (await this.interceptRequest(http)) === true) { return; } - this.download(http, version, this.collection.findOne(params._id)); + await this.download(http, version, await this.collection.findOneAsync(params._id)); } else { next(); } @@ -800,7 +813,7 @@ class FilesCollection extends FilesCollectionCore { // Method used to remove file // from Client side - _methods[this._methodNames._Remove] = function (selector) { + _methods[this._methodNames._Remove] = async function (selector) { check(selector, Match.OneOf(String, Object)); self._debug(`[FilesCollection] [Unlink Method] [.remove(${selector})]`); @@ -809,22 +822,22 @@ class FilesCollection extends FilesCollectionCore { const userId = this.userId; const userFuncs = { userId: this.userId, - user() { + userAsync(){ if (Meteor.users) { - return Meteor.users.findOne(userId); + return Meteor.users.findOneAsync(userId); } return null; } }; - if (!self.onBeforeRemove.call(userFuncs, (self.find(selector) || null))) { + if (!(await self.onBeforeRemove.call(userFuncs, (self.find(selector) || null)))) { throw new Meteor.Error(403, '[FilesCollection] [remove] Not permitted!'); } } const cursor = self.find(selector); - if (cursor.count() > 0) { - self.remove(selector); + if ((await cursor.countAsync()) > 0) { + self.removeAsync(selector); return true; } throw new Meteor.Error(404, 'Cursor is empty, no files is removed'); @@ -840,7 +853,7 @@ class FilesCollection extends FilesCollectionCore { // Basically it prepares everything // So user can pause/disconnect and // continue upload later, during `continueUploadTTL` - _methods[this._methodNames._Start] = function (opts, returnMeta) { + _methods[this._methodNames._Start] = async function (opts, returnMeta) { check(opts, { file: Object, fileId: String, @@ -855,9 +868,9 @@ class FilesCollection extends FilesCollectionCore { self._debug(`[FilesCollection] [File Start Method] ${opts.file.name} - ${opts.fileId}`); opts.___s = true; - const { result } = self._prepareUpload(helpers.clone(opts), this.userId, 'DDP Start Method'); + const { result } = await self._prepareUpload(helpers.clone(opts), this.userId, 'DDP Start Method'); - if (self.collection.findOne(result._id)) { + if (await self.collection.findOneAsync(result._id)) { throw new Meteor.Error(400, 'Can\'t start upload, data substitution detected!'); } @@ -865,7 +878,7 @@ class FilesCollection extends FilesCollectionCore { opts.createdAt = new Date(); opts.maxLength = opts.fileLength; try { - self._preCollection.insert(helpers.omit(opts, '___s')); + await self._preCollection.insertAsync(helpers.omit(opts, '___s')); self._createStream(result._id, result.path, helpers.omit(opts, '___s')); } catch (e) { self._debug(`[FilesCollection] [File Start Method] [EXCEPTION:] ${opts.file.name} - ${opts.fileId}`, e); @@ -885,7 +898,7 @@ class FilesCollection extends FilesCollectionCore { // Method used to write file chunks // it receives very limited amount of meta-data // This method also responsible for EOF - _methods[this._methodNames._Write] = function (_opts) { + _methods[this._methodNames._Write] = async function (_opts) { let opts = _opts; let result; check(opts, { @@ -901,17 +914,17 @@ class FilesCollection extends FilesCollectionCore { opts.binData = Buffer.from(opts.binData, 'base64'); } - const _continueUpload = self._continueUpload(opts.fileId); + const _continueUpload = await self._continueUpload(opts.fileId); if (!_continueUpload) { throw new Meteor.Error(408, 'Can\'t continue upload, session expired. Start upload again.'); } - this.unblock(); - ({result, opts} = self._prepareUpload(Object.assign(opts, _continueUpload), this.userId, 'DDP')); + ({result, opts} = await self._prepareUpload(Object.assign(opts, _continueUpload), this.userId, 'DDP')); if (opts.eof) { try { - return self._handleUploadSync(result, opts); + self._handleUploadSync(result, opts); + return result; } catch (handleUploadErr) { self._debug('[FilesCollection] [Write Method] [DDP] Exception:', handleUploadErr); throw handleUploadErr; @@ -927,10 +940,10 @@ class FilesCollection extends FilesCollectionCore { // - Removing temporary record from @_preCollection // - Removing record from @collection // - .unlink()ing chunks from FS - _methods[this._methodNames._Abort] = function (_id) { + _methods[this._methodNames._Abort] = async function (_id) { check(_id, String); - const _continueUpload = self._continueUpload(_id); + const _continueUpload = await self._continueUpload(_id); self._debug(`[FilesCollection] [Abort Method]: ${_id} - ${(helpers.isObject(_continueUpload.file) ? _continueUpload.file.path : '')}`); if (self._currentUploads && self._currentUploads[_id]) { @@ -939,10 +952,12 @@ class FilesCollection extends FilesCollectionCore { } if (_continueUpload) { - self._preCollection.remove({_id}); - self.remove({_id}); + await self._preCollection.removeAsync({_id}); + await self.removeAsync({_id}); + + if (helpers.isObject(_continueUpload.file) && _continueUpload.file.path) { - self.unlink({_id, path: _continueUpload.file.path}); + await self.unlink({_id, path: _continueUpload.file.path}); } } return true; @@ -957,9 +972,9 @@ class FilesCollection extends FilesCollectionCore { * @memberOf FilesCollection * @name _prepareUpload * @summary Internal method. Used to optimize received data and check upload permission - * @returns {Object} + * @returns {Promise} */ - _prepareUpload(opts = {}, userId, transport) { + async _prepareUpload(opts = {}, userId, transport) { let ctx; if (!helpers.isBoolean(opts.eof)) { opts.eof = false; @@ -1008,21 +1023,21 @@ class FilesCollection extends FilesCollectionCore { }, { chunkId: opts.chunkId, userId: result.userId, - user() { + async userAsync() { if (Meteor.users && result.userId) { - return Meteor.users.findOne(result.userId); + return Meteor.users.findOneAsync(result.userId); } return null; }, eof: opts.eof }); - const isUploadAllowed = this.onBeforeUpload.call(ctx, result); + const isUploadAllowed = await this.onBeforeUpload.call(ctx, result); if (isUploadAllowed !== true) { throw new Meteor.Error(403, helpers.isString(isUploadAllowed) ? isUploadAllowed : '@onBeforeUpload() returned false'); } else { if ((opts.___s === true) && this.onInitiateUpload && helpers.isFunction(this.onInitiateUpload)) { - this.onInitiateUpload.call(ctx, result); + await this.onInitiateUpload.call(ctx, result); } } } else if ((opts.___s === true) && this.onInitiateUpload && helpers.isFunction(this.onInitiateUpload)) { @@ -1031,15 +1046,15 @@ class FilesCollection extends FilesCollectionCore { }, { chunkId: opts.chunkId, userId: result.userId, - user() { + async userAsync() { if (Meteor.users && result.userId) { - return Meteor.users.findOne(result.userId); + return Meteor.users.findOneAsync(result.userId); } return null; }, eof: opts.eof }); - this.onInitiateUpload.call(ctx, result); + await this.onInitiateUpload.call(ctx, result); } return {result, opts}; @@ -1050,34 +1065,35 @@ class FilesCollection extends FilesCollectionCore { * @memberOf FilesCollection * @name _finishUpload * @summary Internal method. Finish upload, close Writable stream, add record to MongoDB and flush used memory - * @returns {undefined} + * @returns {Promise} */ - _finishUpload(result, opts, cb) { + async _finishUpload(result, opts, cb) { this._debug(`[FilesCollection] [Upload] [finish(ing)Upload] -> ${result.path}`); fs.chmod(result.path, this.permissions, noop); result.type = this._getMimeType(opts.file); result.public = this.public; this._updateFileTypes(result); - this.collection.insert(helpers.clone(result), (colInsert, _id) => { - if (colInsert) { - cb && cb(colInsert); - this._debug('[FilesCollection] [Upload] [_finishUpload] [insert] Error:', colInsert); - } else { - this._preCollection.update({_id: opts.fileId}, {$set: {isFinished: true}}, (preUpdateError) => { - if (preUpdateError) { - cb && cb(preUpdateError); - this._debug('[FilesCollection] [Upload] [_finishUpload] [update] Error:', preUpdateError); - } else { - result._id = _id; - this._debug(`[FilesCollection] [Upload] [finish(ed)Upload] -> ${result.path}`); - this.onAfterUpload && this.onAfterUpload.call(this, result); - this.emit('afterUpload', result); - cb && cb(null, result); - } - }); + let _id; + try { + _id = await this.collection.insertAsync(helpers.clone(result)); + try { + await this._preCollection.updateAsync({_id: opts.fileId}, {$set: {isFinished: true}}); + if (_id) result._id = _id; + this._debug(`[FilesCollection] [Upload] [finish(ed)Upload] -> ${result.path}`); + if (this.onAfterUpload && helpers.isFunction(this.onAfterUpload)) { + await this.onAfterUpload.call(this, result); + } + this.emit('afterUpload', result); + cb(null, result); + } catch (prrUpdateError) { + cb(prrUpdateError); + this._debug('[FilesCollection] [Upload] [_finishUpload] [update] Error:', prrUpdateError); } - }); + } catch (colInsert){ + cb(colInsert); + this._debug('[FilesCollection] [Upload] [_finishUpload] [insert] Error:', colInsert); + } } /** @@ -1170,6 +1186,7 @@ class FilesCollection extends FilesCollectionCore { */ _getUserDefault(http) { const result = { + async userAsync() { return null; }, user() { return null; }, userId: null }; @@ -1189,7 +1206,7 @@ class FilesCollection extends FilesCollectionCore { const userId = this._getUserId(mtok); if (userId) { - result.user = () => Meteor.users.findOne(userId); + result.userAsync = () => Meteor.users.findOneAsync(userId); result.userId = userId; } } @@ -1209,29 +1226,22 @@ class FilesCollection extends FilesCollectionCore { * @param {Object} opts.meta - File additional meta-data * @param {String} opts.userId - UserId, default *null* * @param {String} opts.fileId - _id, sanitized, max-length: 20; default *null* - * @param {Function} callback - function(error, fileObj){...} * @param {Boolean} proceedAfterUpload - Proceed onAfterUpload hook * @summary Write buffer to FS and add to FilesCollection Collection - * @returns {FilesCollection} Instance + * @throws {Meteor.Error} If there is an error writing the file or inserting the document + * @returns {Promise} Instance */ - write(buffer, _opts = {}, _callback, _proceedAfterUpload) { - this._debug('[FilesCollection] [write()]'); + async write(buffer, _opts = {}, _proceedAfterUpload) { + this._debug('[FilesCollection] [writeAsync()]'); let opts = _opts; - let callback = _callback; let proceedAfterUpload = _proceedAfterUpload; - if (helpers.isFunction(opts)) { - proceedAfterUpload = callback; - callback = opts; - opts = {}; - } else if (helpers.isBoolean(callback)) { - proceedAfterUpload = callback; - } else if (helpers.isBoolean(opts)) { + if (helpers.isBoolean(opts)) { proceedAfterUpload = opts; + opts = {}; } check(opts, Match.Optional(Object)); - check(callback, Match.Optional(Function)); check(proceedAfterUpload, Match.Optional(Boolean)); opts.fileId = opts.fileId && this.sanitize(opts.fileId, 20, 'a'); @@ -1263,41 +1273,53 @@ class FilesCollection extends FilesCollectionCore { result._id = fileId; - fs.stat(opts.path, (statError, stats) => { - bound(() => { - if (statError || !stats.isFile()) { - const paths = opts.path.split('/'); - paths.pop(); - fs.mkdirSync(paths.join('/'), { recursive: true }); - fs.writeFileSync(opts.path, ''); - } + let fileRef; - const stream = fs.createWriteStream(opts.path, {flags: 'w', mode: this.permissions}); - stream.end(buffer, (streamErr) => { - bound(() => { - if (streamErr) { - callback && callback(streamErr); - } else { - this.collection.insert(result, (insertErr, _id) => { - if (insertErr) { - callback && callback(insertErr); - this._debug(`[FilesCollection] [write] [insert] Error: ${fileName} -> ${this.collectionName}`, insertErr); - } else { - const fileRef = this.collection.findOne(_id); - callback && callback(null, fileRef); - if (proceedAfterUpload === true) { - this.onAfterUpload && this.onAfterUpload.call(this, fileRef); - this.emit('afterUpload', fileRef); - } - this._debug(`[FilesCollection] [write]: ${fileName} -> ${this.collectionName}`); - } - }); - } - }); - }); + let mustCreateFileFirst = false; + try { + const stats = await fs.promises.stat(opts.path); + if (!stats.isFile()) { + mustCreateFileFirst = true; + } + } catch (statError) { + mustCreateFileFirst = true; + } + if (mustCreateFileFirst) { + const paths = opts.path.split('/'); + paths.pop(); + await fs.promises.mkdir(paths.join('/'), { recursive: true }); + await fs.promises.writeFile(opts.path, ''); + } + + const stream = fs.createWriteStream(opts.path, {flags: 'w', mode: this.permissions}); + + await new Promise((resolve, reject) => { + stream.end(buffer, (streamErr) => { + if (streamErr) { + reject(streamErr); + } else { + resolve(); + } }); }); - return this; + + try { + const _id = await this.collection.insertAsync(result); + fileRef = await this.collection.findOneAsync(_id); + + if (proceedAfterUpload === true) { + if (this.onAfterUpload){ + await this.onAfterUpload.call(this, fileRef); + } + this.emit('afterUploadAsync', fileRef); + } + this._debug(`[FilesCollection] [write]: ${fileName} -> ${this.collectionName}`); + } catch (insertErr) { + this._debug(`[FilesCollection] [write] [insert] Error: ${fileName} -> ${this.collectionName}`, insertErr); + throw new Meteor.Error('writeAsync', insertErr); + } + + return fileRef; } /** @@ -1316,27 +1338,20 @@ class FilesCollection extends FilesCollectionCore { * @param {Function} callback - function(error, fileObj){...} * @param {Boolean} [proceedAfterUpload] - Proceed onAfterUpload hook * @summary Download file over HTTP, write stream to FS, and add to FilesCollection Collection - * @returns {FilesCollection} Instance + * @returns {Promise} File Object */ - load(url, _opts = {}, _callback, _proceedAfterUpload = false) { - this._debug(`[FilesCollection] [load(${url}, ${JSON.stringify(_opts)}, callback)]`); + async load(url, _opts = {}, _proceedAfterUpload = false) { + this._debug(`[FilesCollection] [loadAsync(${url}, ${JSON.stringify(_opts)}, callback)]`); let opts = _opts; - let callback = _callback; let proceedAfterUpload = _proceedAfterUpload; - if (helpers.isFunction(opts)) { - proceedAfterUpload = callback; - callback = opts; + if (helpers.isBoolean(_opts)) { + proceedAfterUpload = _opts; opts = {}; - } else if (helpers.isBoolean(callback)) { - proceedAfterUpload = callback; - } else if (helpers.isBoolean(opts)) { - proceedAfterUpload = opts; } check(url, String); check(opts, Match.Optional(Object)); - check(callback, Match.Optional(Function)); check(proceedAfterUpload, Match.Optional(Boolean)); if (!helpers.isObject(opts)) { @@ -1357,134 +1372,101 @@ class FilesCollection extends FilesCollectionCore { const {extension, extensionWithDot} = this._getExt(fileName); opts.path = `${this.storagePath(opts)}${nodePath.sep}${fsName}${extensionWithDot}`; - const storeResult = (result, cb) => { + // this will be the resolved fileRef + let fileRef; + + // storeResult is a function that will be called after the file is downloaded and stored in the database + // this might throw an error from collection.insertAsync or collection.findOneAsync + const storeResult = async (result) => { result._id = fileId; + const _id = await this.collection.insertAsync(result); - this.collection.insert(result, (error, _id) => { - if (error) { - cb && cb(error); - this._debug(`[FilesCollection] [load] [insert] Error: ${fileName} -> ${this.collectionName}`, error); - } else { - const fileRef = this.collection.findOne(_id); - cb && cb(null, fileRef); - if (proceedAfterUpload === true) { - this.onAfterUpload && this.onAfterUpload.call(this, fileRef); - this.emit('afterUpload', fileRef); - } - this._debug(`[FilesCollection] [load] [insert] ${fileName} -> ${this.collectionName}`); + fileRef = await this.collection.findOneAsync(_id); + if (proceedAfterUpload === true) { + if (this.onAfterUpload){ + await this.onAfterUpload.call(this, fileRef); } - }); + this.emit('afterUploadAsync', fileRef); + } + this._debug(`[FilesCollection] [load] [insert] ${fileName} -> ${this.collectionName}`); }; - fs.stat(opts.path, (statError, stats) => { - bound(() => { - if (statError || !stats.isFile()) { - const paths = opts.path.split('/'); - paths.pop(); - fs.mkdirSync(paths.join('/'), { recursive: true }); - fs.writeFileSync(opts.path, ''); - } + // check if the file already exists, otherwise create it + let mustCreateFileFirst = false; + try { + const stats = await fs.promises.stat(opts.path); + if (!stats.isFile()) { + mustCreateFileFirst = true; + } + } catch (statError) { + mustCreateFileFirst = true; + } + if(mustCreateFileFirst) { + const paths = opts.path.split('/'); + paths.pop(); + fs.mkdirSync(paths.join('/'), { recursive: true }); + fs.writeFileSync(opts.path, ''); + } - let isEnded = false; - let timer = null; - const wStream = fs.createWriteStream(opts.path, {flags: 'w', mode: this.permissions, autoClose: true, emitClose: false }); - const onEnd = (_error, response) => { - if (!isEnded) { - if (timer) { - Meteor.clearTimeout(timer); - timer = null; - } + const wStream = fs.createWriteStream(opts.path, {flags: 'w', mode: this.permissions, autoClose: true, emitClose: false }); + const controller = new AbortController(); - isEnded = true; - if (response && response.status === 200) { - this._debug(`[FilesCollection] [load] Received: ${url}`); - const result = this._dataToSchema({ - name: fileName, - path: opts.path, - meta: opts.meta, - type: opts.type || response.headers.get('content-type') || this._getMimeType({path: opts.path}), - size: opts.size || parseInt(response.headers.get('content-length') || 0), - userId: opts.userId, - extension - }); - - if (!result.size) { - fs.stat(opts.path, (statErrorOnEnd, newStats) => { - bound(() => { - if (statErrorOnEnd) { - callback && callback(statErrorOnEnd); - } else { - result.versions.original.size = (result.size = newStats.size); - storeResult(result, callback); - } - }); - }); - } else { - storeResult(result, callback); - } - } else { - const error = _error || new Meteor.Error(response?.status || 408, response?.statusText || 'Bad response with empty details'); - this._debug(`[FilesCollection] [load] [fetch(${url})] Error:`, error); + try { + let timer; - if (!wStream.destroyed) { - wStream.destroy(); - } + if (opts.timeout > 0) { + timer = Meteor.setTimeout(() => { + controller.abort(); + throw new Meteor.Error(408, `Request timeout after ${opts.timeout}ms`); + }, opts.timeout); + } - fs.unlink(opts.path, (unlinkError) => { - bound(() => { - callback && callback(error); - if (unlinkError) { - this._debug(`[FilesCollection] [load] [fetch(${url})] [fs.unlink(${opts.path})] unlinkError:`, unlinkError); - } - }); - }); - } - } - }; + const res = await fetch(url, { + headers: opts.headers || {}, + signal: controller.signal + }); - let resp = void 0; - wStream.on('error', (error) => { - bound(() => { - onEnd(error); - }); - }); - wStream.on('close', () => { - bound(() => { - onEnd(void 0, resp); - }); - }); - wStream.on('finish', () => { - bound(() => { - onEnd(void 0, resp); - }); - }); + if (timer) { + Meteor.clearTimeout(timer); + timer = null; + } - const controller = new AbortController(); - fetch(url, { - headers: opts.headers || {}, - signal: controller.signal - }).then((res) => { - resp = res; - res.body.on('error', (error) => { - bound(() => { - onEnd(error); - }); - }); - res.body.pipe(wStream); - }).catch((fetchError) => { - onEnd(fetchError); - }); + if (!res.ok) { + throw new Error(`Unexpected response ${res.statusText}`); + } - if (opts.timeout > 0) { - timer = Meteor.setTimeout(() => { - onEnd(new Meteor.Error(408, `Request timeout after ${opts.timeout}ms`)); - controller.abort(); - }, opts.timeout); - } + await pipeline(res.body, wStream); + + const result = this._dataToSchema({ + name: fileName, + path: opts.path, + meta: opts.meta, + type: opts.type || res.headers.get('content-type') || this._getMimeType({path: opts.path}), + size: opts.size || parseInt(res.headers.get('content-length') || 0), + userId: opts.userId, + extension }); - }); - return this; + if (!result.size) { + const newStats = await fs.promises.stat(opts.path); + result.versions.original.size = (result.size = newStats.size); + await storeResult(result); + } else { + await storeResult(result); + } + res.body.pipe(wStream); + } catch(error){ + this._debug(`[FilesCollection] [loadAsync] [fetch(${url})] Error:`, error); + + if (fs.existsSync(opts.path)) { + await fs.promises.unlink(opts.path); + } + + throw error; + } + + + return fileRef; } /** @@ -1498,133 +1480,134 @@ class FilesCollection extends FilesCollectionCore { * @param {String} opts.fileId - _id, sanitized, max-length: 20 symbols default *null* * @param {Object} opts.fileName - [Optional] File name, if not specified file name and extension will be taken from path * @param {String} opts.userId - [Optional] UserId, default *null* - * @param {Function} callback - [Optional] function(error, fileObj){...} * @param {Boolean} proceedAfterUpload - Proceed onAfterUpload hook * @summary Add file from FS to FilesCollection - * @returns {FilesCollection} Instance + * @throws {Meteor.Error} If file does not exist (400) or collection is public (403) + * @returns {Promise} Instance */ - addFile(path, _opts = {}, _callback, _proceedAfterUpload) { + async addFile(path, _opts = {}, _proceedAfterUpload) { this._debug(`[FilesCollection] [addFile(${path})]`); let opts = _opts; - let callback = _callback; let proceedAfterUpload = _proceedAfterUpload; - if (helpers.isFunction(opts)) { - proceedAfterUpload = callback; - callback = opts; - opts = {}; - } else if (helpers.isBoolean(callback)) { - proceedAfterUpload = callback; - } else if (helpers.isBoolean(opts)) { - proceedAfterUpload = opts; - } - if (this.public) { - throw new Meteor.Error(403, 'Can not run [addFile] on public collection! Just Move file to root of your server, then add record to Collection'); + throw new Meteor.Error( + 403, + 'Can not run [addFile] on public collection! Just Move file to root of your server, then add record to Collection' + ); } check(path, String); check(opts, Match.Optional(Object)); - check(callback, Match.Optional(Function)); check(proceedAfterUpload, Match.Optional(Boolean)); - fs.stat(path, (statErr, stats) => bound(() => { - if (statErr) { - callback && callback(statErr); - } else if (stats.isFile()) { - if (!helpers.isObject(opts)) { - opts = {}; - } - opts.path = path; + let stats; + try { + stats = await fs.promises.stat(path); + } catch (statErr) { + if (statErr.code === 'ENOENT') { + throw new Meteor.Error( + 400, + `[FilesCollection] [addFile(${path})]: File does not exist` + ); + } + throw new Meteor.Error(statErr.code, statErr.message); + } + if (stats.isFile()) { + if (!helpers.isObject(opts)) { + opts = {}; + } + opts.path = path; - if (!opts.fileName) { - const pathParts = path.split(nodePath.sep); - opts.fileName = path.split(nodePath.sep)[pathParts.length - 1]; - } + if (!opts.fileName) { + const pathParts = path.split(nodePath.sep); + opts.fileName = path.split(nodePath.sep)[pathParts.length - 1]; + } - const {extension} = this._getExt(opts.fileName); + const { extension } = this._getExt(opts.fileName); - if (!helpers.isString(opts.type)) { - opts.type = this._getMimeType(opts); - } + if (!helpers.isString(opts.type)) { + opts.type = this._getMimeType(opts); + } - if (!helpers.isObject(opts.meta)) { - opts.meta = {}; - } + if (!helpers.isObject(opts.meta)) { + opts.meta = {}; + } - if (!helpers.isNumber(opts.size)) { - opts.size = stats.size; - } + if (!helpers.isNumber(opts.size)) { + opts.size = stats.size; + } - const result = this._dataToSchema({ - name: opts.fileName, - path, - meta: opts.meta, - type: opts.type, - size: opts.size, - userId: opts.userId, - extension, - _storagePath: path.replace(`${nodePath.sep}${opts.fileName}`, ''), - fileId: (opts.fileId && this.sanitize(opts.fileId, 20, 'a')) || null - }); + const result = this._dataToSchema({ + name: opts.fileName, + path, + meta: opts.meta, + type: opts.type, + size: opts.size, + userId: opts.userId, + extension, + _storagePath: path.replace(`${nodePath.sep}${opts.fileName}`, ''), + fileId: (opts.fileId && this.sanitize(opts.fileId, 20, 'a')) || null, + }); + let _id; + try { + _id = await this.collection.insertAsync(result); + } catch (insertErr) { + this._debug( + `[FilesCollection] [addFileAsync] [insertAsync] Error: ${result.name} -> ${this.collectionName}`, + insertErr + ); + throw new Meteor.Error(insertErr.code, insertErr.message); + } - this.collection.insert(result, (insertErr, _id) => { - if (insertErr) { - callback && callback(insertErr); - this._debug(`[FilesCollection] [addFile] [insert] Error: ${result.name} -> ${this.collectionName}`, insertErr); - } else { - const fileRef = this.collection.findOne(_id); - callback && callback(null, fileRef); - if (proceedAfterUpload === true) { - this.onAfterUpload && this.onAfterUpload.call(this, fileRef); - this.emit('afterUpload', fileRef); - } - this._debug(`[FilesCollection] [addFile]: ${result.name} -> ${this.collectionName}`); - } - }); - } else { - callback && callback(new Meteor.Error(400, `[FilesCollection] [addFile(${path})]: File does not exist`)); + const fileRef = await this.collection.findOneAsync(_id); + + if (proceedAfterUpload === true) { + this.onAfterUpload && this.onAfterUpload.call(this, fileRef); + this.emit('afterUpload', fileRef); } - })); - return this; + this._debug( + `[FilesCollection] [addFileAsync]: ${result.name} -> ${this.collectionName}` + ); + return fileRef; + } + throw new Meteor.Error( + 400, + `[FilesCollection] [addFile(${path})]: File does not exist` + ); } /** * @locus Anywhere * @memberOf FilesCollection - * @name remove + * @name removeAsync * @param {String|Object} selector - Mongo-Style selector (http://docs.meteor.com/api/collections.html#selectors) - * @param {Function} callback - Callback with one `error` argument + * @throws {Meteor.Error} If cursor is empty * @summary Remove documents from the collection - * @returns {FilesCollection} Instance + * @returns {Promise} Instance */ - remove(selector, callback) { - this._debug(`[FilesCollection] [remove(${JSON.stringify(selector)})]`); + async removeAsync(selector) { + this._debug(`[FilesCollection] [removeAsync(${JSON.stringify(selector)})]`); if (selector === void 0) { return 0; } - check(callback, Match.Optional(Function)); const files = this.collection.find(selector); - if (files.count() > 0) { - files.forEach((file) => { + if (await files.countAsync() > 0) { + await files.forEachAsync((file) => { this.unlink(file); }); } else { - callback && callback(new Meteor.Error(404, 'Cursor is empty, no files is removed')); - return this; + throw new Meteor.Error(404, 'Cursor is empty, no files is removed'); } if (this.onAfterRemove) { - const docs = files.fetch(); - const self = this; - this.collection.remove(selector, function () { - callback && callback.apply(this, arguments); - self.onAfterRemove(docs); - }); + const docs = await files.fetchAsync(); + await this.collection.removeAsync(selector); + await this.onAfterRemove(docs); } else { - this.collection.remove(selector, (callback || noop)); + await this.collection.removeAsync(selector); } return this; } @@ -1753,14 +1736,19 @@ class FilesCollection extends FilesCollectionCore { * @param {String} version - Requested file version * @param {Object} fileRef - Requested file Object * @summary Initiates the HTTP response - * @returns {undefined} + * @returns {Promise} */ - download(http, version = 'original', fileRef) { + async download(http, version = 'original', fileRef) { let vRef; - this._debug(`[FilesCollection] [download(${http.request.originalUrl}, ${version})]`); + this._debug( + `[FilesCollection] [download(${http.request.originalUrl}, ${version})]` + ); if (fileRef) { - if (helpers.has(fileRef, 'versions') && helpers.has(fileRef.versions, version)) { + if ( + helpers.has(fileRef, 'versions') && + helpers.has(fileRef.versions, version) + ) { vRef = fileRef.versions[version]; vRef._id = fileRef._id; } else { @@ -1773,30 +1761,55 @@ class FilesCollection extends FilesCollectionCore { if (!vRef || !helpers.isObject(vRef)) { return this._404(http); } else if (fileRef) { - if (helpers.isFunction(this.downloadCallback) && !this.downloadCallback.call(Object.assign(http, this._getUser(http)), fileRef)) { + if ( + helpers.isFunction(this.downloadCallback) && + !(await this.downloadCallback( + Object.assign(http, this._getUser(http)), + fileRef + )) + ) { return this._404(http); } - if (this.interceptDownload && helpers.isFunction(this.interceptDownload) && this.interceptDownload(http, fileRef, version) === true) { + if ( + this.interceptDownload && + helpers.isFunction(this.interceptDownload) && + (await this.interceptDownload(http, fileRef, version)) === true + ) { return void 0; } - fs.stat(vRef.path, (statErr, stats) => bound(() => { - let responseType; - if (statErr || !stats.isFile()) { + let stats; + + try { + stats = await fs.promises.stat(vRef.path); + } catch (statErr){ + if (statErr) { return this._404(http); } + } + if (!stats.isFile()) { + return this._404(http); + } + let responseType; - if ((stats.size !== vRef.size) && !this.integrityCheck) { - vRef.size = stats.size; - } + if (stats.size !== vRef.size && !this.integrityCheck) { + vRef.size = stats.size; + } - if ((stats.size !== vRef.size) && this.integrityCheck) { - responseType = '400'; - } + if (stats.size !== vRef.size && this.integrityCheck) { + responseType = '400'; + } + + this.serve( + http, + fileRef, + vRef, + version, + null, + responseType || '200' + ); - return this.serve(http, fileRef, vRef, version, null, (responseType || '200')); - })); return void 0; } return this._404(http); @@ -1825,7 +1838,7 @@ class FilesCollection extends FilesCollectionCore { let take; let responseType = _responseType; - if (http.params.query.download && (http.params.query.download === 'true')) { + if (http.params?.query?.download && (http.params.query.download === 'true')) { dispositionType = 'attachment; '; } else { dispositionType = 'inline; '; @@ -1853,7 +1866,7 @@ class FilesCollection extends FilesCollectionCore { take = vRef.size; } - if (partiral || (http.params.query.play && (http.params.query.play === 'true'))) { + if (partiral || (http.params?.query?.play && (http.params.query.play === 'true'))) { reqRange = {start, end}; if (isNaN(start) && !isNaN(end)) { reqRange.start = end - take; diff --git a/tests/core.test.js b/tests/core.test.js new file mode 100644 index 0000000..45eae1b --- /dev/null +++ b/tests/core.test.js @@ -0,0 +1,172 @@ +/* global describe, beforeEach, it */ +import { expect, assert } from 'chai'; +import FilesCollectionCore from '../core.js'; +import { FileCursor, FilesCursor } from '../cursor.js'; +import { FilesCollection } from '../server.js'; + +describe('FilesCollectionCore', function() { + let filesCollectionCore; + + beforeEach(function() { + filesCollectionCore = new FilesCollectionCore(); + }); + + describe('_getFileName', function() { + it('should return the correct file name', function() { + const fileData = { name: 'test.txt' }; + const result = filesCollectionCore._getFileName(fileData); + expect(result).to.equal('test.txt'); + }); + }); + + describe('_getExt', function() { + it('should return the correct file extension', function() { + const result = filesCollectionCore._getExt('test.txt'); + expect(result).to.deep.equal({ ext: 'txt', extension: 'txt', extensionWithDot: '.txt' }); + }); + }); + + describe('_updateFileTypes', function() { + it('should correctly classify file types', function() { + const data = { type: 'video/mp4' }; + filesCollectionCore._updateFileTypes(data); + expect(data.isVideo).to.be.true; + expect(data.isAudio).to.be.false; + expect(data.isImage).to.be.false; + expect(data.isText).to.be.false; + expect(data.isJSON).to.be.false; + expect(data.isPDF).to.be.false; + }); + }); + + describe('_dataToSchema', function() { + it('should create a schema object from the given data', function() { + const core = new FilesCollectionCore(); + const data = { + fileId: 'file1', + name: 'test', + extension: 'txt', + path: '/path/to/test', + meta: {}, + type: 'text/plain', + size: 100, + userId: 'user1', + _downloadRoute: '/download', + _collectionName: 'testCollection', + _storagePath: '/storage/path' + }; + + const expectedSchema = { + fileId: 'file1', + name: 'test', + extension: 'txt', + ext: 'txt', + extensionWithDot: '.txt', + path: '/path/to/test', + meta: {}, + type: 'text/plain', + mime: 'text/plain', + 'mime-type': 'text/plain', + size: 100, + userId: 'user1', + versions: { + original: { + path: '/path/to/test', + size: 100, + type: 'text/plain', + extension: 'txt', + }, + }, + _downloadRoute: '/download', + _collectionName: 'testCollection', + _id: 'file1', + _storagePath: '/storage/path', + }; + + const schema = core._dataToSchema(data); + assert.deepStrictEqual(schema, filesCollectionCore._dataToSchema(expectedSchema)); + }); + }); + + describe('#findOneAsync()', function() { + it('should find and return a FileCursor for matching document Object', async function() { + const core = new FilesCollectionCore(); + const selector = { name: 'test' }; + const options = {}; + + // Mock the collection.findOneAsync method to return a dummy document + core.collection = { + findOneAsync: async (sel, opts) => { + expect(sel).to.deep.equal(selector); + expect(opts).to.deep.equal(options); + return { name: 'test' }; + } + }; + + const doc = await core.findOneAsync(selector, options); + expect(doc).to.be.an.instanceof(FileCursor); + expect(doc).to.deep.equal(new FileCursor({ name: 'test' }, core)); + }); + + it('should return null if no document is found', async function() { + const core = new FilesCollectionCore(); + const selector = { name: 'nonexistent' }; + const options = {}; + + // Mock the collection.findOneAsync method to return null + core.collection = { + findOneAsync: async (sel, opts) => { + expect(sel).to.deep.equal(selector); + expect(opts).to.deep.equal(options); + return null; + } + }; + + const doc = await core.findOneAsync(selector, options); + expect(doc).to.be.null; + }); + }); + + describe('#find()', function() { + it('should find and return a FilesCursor for matching documents', function() { + // Testing with FilesCollectionCore instance only fails, due to lack of a + // underlying collection, so we use the FilesCollection class + + const collection = new FilesCollection({ collectionName: 'test' }); + const selector = { name: 'test' }; + const options = {}; + + const cursor = collection.find(selector, options); + expect(cursor).to.be.an.instanceof(FilesCursor); + }); + }); + + describe('#updateAsync()', function() { + it('should call the collection.updateAsync method with the given arguments', async function() { + const core = new FilesCollectionCore(); + const selector = { name: 'test' }; + const modifier = { $set: { name: 'newTest' } }; + + // Mock the collection.updateAsync method to check the arguments + core.collection = { + updateAsync: async (sel, mod) => { + expect(sel).to.deep.equal(selector); + expect(mod).to.deep.equal(modifier); + } + }; + + await core.updateAsync(selector, modifier); + }); + }); + + describe('#link()', function() { + it('should return a downloadable URL for the given file reference and version', function() { + const core = new FilesCollectionCore(); + const fileRef = { _id: 'test' }; + const version = 'original'; + + const url = core.link(fileRef, version); + expect(url).to.be.a('string'); + }); + }); +}); diff --git a/tests/cursor.test.js b/tests/cursor.test.js new file mode 100644 index 0000000..712b90a --- /dev/null +++ b/tests/cursor.test.js @@ -0,0 +1,319 @@ +/* global describe, beforeEach, after, before it, afterEach, Meteor */ +import { expect } from 'chai'; +import sinon from 'sinon'; +import FilesCollectionCore from '../core.js'; +import { FileCursor, FilesCursor } from '../cursor.js'; +import { FilesCollection } from '../server.js'; +import fs from 'fs'; +import { MongoInternals } from 'meteor/mongo'; + + +describe('FileCursor', function() { + let collectionName = 'FileCursor'; + let filesCollection; + + before(function() { + filesCollection = new FilesCollection({ collectionName }); + }); + + after(async function() { + await MongoInternals.defaultRemoteCollectionDriver().mongo.db.collection(collectionName).drop(); + }); + + beforeEach(async function() { + await filesCollection.collection.rawCollection().deleteMany({}); + sinon.restore(); + }); + + afterEach(function() { + sinon.restore(); + }); + + describe('#removeAsync()', function() { + let sandbox; + beforeEach(function() { + sandbox = sinon.createSandbox(); + }); + afterEach(function() { + sandbox.restore(); + }); + it('should call the collection.removeAsync with the file ID and unlink method with the path', async function() { + const fileRef = { _id: 'test', path: '/tmp' }; + await filesCollection.collection.rawCollection().insertOne(fileRef); + + const removeAsync = sandbox.stub(filesCollection.collection, 'removeAsync').resolves('test'); + const unlink = sandbox.stub(fs, 'unlink').resolves('test'); + + const cursor = new FileCursor(fileRef, filesCollection); + + await cursor.removeAsync(); + expect(removeAsync.calledWith(fileRef._id)).to.be.true; + expect(unlink.calledWith(fileRef.path)).to.be.true; + }); + + it('should call the callback with an error if no file reference is provided', async function() { + const core = new FilesCollectionCore(); + const cursor = new FileCursor(null, core); + fs.writeFileSync('/tmp/test.txt', 'test'); + const opts = { _id: 'test' }; + await filesCollection.addFile('/tmp/test.txt', opts); + let error; + try { + await cursor.removeAsync(); + } catch (err) { + error = err; + } + expect(error).to.be.instanceOf(Meteor.Error); + expect(error.reason).to.equal('No such file'); + fs.unlinkSync('/tmp/test.txt'); + }); + }); + + describe('#link()', function() { + it('should call the collection.link method with the fileRef, version and uriBase', function() { + const fileRef = { _id: 'test' }; + const version = 'v1'; + const uriBase = 'https://test.com'; + + // Mock the collection.remove method to check the arguments + filesCollection.link = sinon.spy(); + + const cursor = new FileCursor(fileRef, filesCollection); + cursor.link(() => { + expect(filesCollection.link.calledWith(fileRef, version, uriBase)).to.be.true; + }); + }); + + it('should call the callback with an error if no file reference is provided', function() { + const cursor = new FileCursor(null, filesCollection); + cursor.link((err) => { + expect(err).to.be.instanceOf(Meteor.Error); + expect(err.reason).to.equal('No such file'); + }); + }); + }); +}); + +describe('FilesCursor', function() { + let collectionName = 'FilesCursor'; + let filesCollection; + let sandbox; + + before(function() { + filesCollection = new FilesCollection({ collectionName }); + }); + + after(async function() { + await MongoInternals.defaultRemoteCollectionDriver().mongo.db.collection(collectionName).drop(); + }); + + beforeEach(async function() { + await filesCollection.collection.rawCollection().deleteMany({}); + sandbox = sinon.createSandbox(); + sinon.restore(); + }); + + afterEach(function() { + sinon.restore(); + sandbox.restore(); + }); + + describe('#get()', function() { + it('should return all matching documents as an array', async function() { + const documents = [{ _id: 'test1' }, { _id: 'test2' }]; + + await filesCollection.collection.rawCollection().insertMany(documents); + + const cursor = new FilesCursor({}, {}, filesCollection); + const fetched = await cursor.get(); + expect(fetched).to.deep.equal(documents); + }); + }); + + describe('#hasNext()', function() { + it('should return true if there is a next item available on the cursor', async function() { + // Mock the collection.find method to return a cursor with a countAsync method + sandbox.stub(filesCollection.collection, 'find').returns({ + countAsync: async () => 2, + }); + + const cursor = new FilesCursor({}, {}, filesCollection); + const hasNext = await cursor.hasNext(); + expect(hasNext).to.be.true; + }); + + it('should return false if there is no next item available on the cursor', async function() { + // Mock the collection.find method to return a cursor with a countAsync method + sandbox.stub(filesCollection.collection, 'find').returns({ + countAsync: async () => 0, + }); + + + const cursor = new FilesCursor({}, {}, filesCollection); + const hasNext = await cursor.hasNext(); + expect(hasNext).to.be.false; + }); + }); + + describe('#next()', function() { + it('should return the next item on the cursor', async function() { + const documents = [{ _id: 'test1' }, { _id: 'test2' }]; + await filesCollection.collection.rawCollection().insertMany(documents); + + const cursor = new FilesCursor({}, {}, filesCollection); + + let next = await cursor.next(); + expect(next).to.deep.equal(documents[0]); + + next = await cursor.next(); + expect(next).to.deep.equal(documents[1]); + }); + }); + + describe('#hasPrevious()', function() { + it('should return true if there is a previous item available on the cursor', function() { + const cursor = new FilesCursor({}, {}, filesCollection); + cursor._current = 1; + const hasPrevious = cursor.hasPrevious(); + expect(hasPrevious).to.be.true; + }); + + it('should return false if there is no previous item available on the cursor', function() { + const cursor = new FilesCursor({}, {}, filesCollection); + cursor._current = -1; + const hasPrevious = cursor.hasPrevious(); + expect(hasPrevious).to.be.false; + }); + }); + + describe('#previous()', function() { + it('should return the previous item on the cursor', function() { + const cursor = new FilesCursor({}, {}, filesCollection); + const documents = [{ _id: 'test1' }, { _id: 'test2' }]; + cursor._current = 1; + sandbox.stub(cursor.cursor, 'fetchAsync').resolves(documents); + cursor.previous(); + expect(cursor._current).to.equal(0); + }); + }); + + describe('#fetchAsync()', function() { + it('should return all matching documents as an array', async function() { + const cursor = new FilesCursor({}, {}, filesCollection); + const documents = [{ _id: 'test1' }, { _id: 'test2' }]; + sandbox.stub(cursor.cursor, 'fetchAsync').returns(Promise.resolve(documents)); + const result = await cursor.fetchAsync(); + expect(result).to.deep.equal(documents); + }); + + it('should return an empty array if no matching documents are found', async function() { + const cursor = new FilesCursor({}, {}, filesCollection); + sandbox.stub(cursor.cursor, 'fetchAsync').returns(Promise.resolve(null)); + const result = await cursor.fetchAsync(); + expect(result).to.deep.equal([]); + }); + }); + + describe('#last()', function() { + it('should return the last item on the cursor', async function() { + const cursor = new FilesCursor({}, {}, filesCollection); + const documents = [{ _id: 'test1' }, { _id: 'test2' }]; + await filesCollection.collection.rawCollection().insertMany(documents); + + const last = await cursor.last(); + expect(last).to.deep.equal(documents[1]); + }); + }); + + describe('#countAsync()', function() { + it('should return the number of documents that match a query', async function() { + const cursor = new FilesCursor({}, {}, filesCollection); + sandbox.stub(cursor.cursor, 'countAsync').returns(Promise.resolve(2)); + const count = await cursor.countAsync(); + expect(count).to.equal(2); + }); + }); + + describe('#removeAsync()', function() { + it('should remove all matching documents', async function() { + const cursor = new FilesCursor({_id: 'test1'}, {}, filesCollection); + const documents = [{ _id: 'test1', path: '/tmp/random' }, { _id: 'test2', path: '/tmp/random' }]; + await filesCollection.collection.rawCollection().insertMany(documents); + + await cursor.removeAsync(); + + const result = await filesCollection.collection.rawCollection().find().toArray(); + expect(result).to.have.lengthOf(1); + }); + }); + + describe('#forEachAsync()', function() { + it('should call the callback for each matching document', async function() { + const cursor = new FilesCursor({}, {}, filesCollection); + const documents = [{ _id: 'test1' }, { _id: 'test2' }]; + await filesCollection.collection.rawCollection().insertMany(documents); + let count = 0; + await cursor.forEachAsync(() => { + count++; + }); + expect(count).to.equal(documents.length); + }); + }); + + describe('#each()', function() { + it('should return an array of FileCursor for each document', async function() { + const cursor = new FilesCursor({}, {}, filesCollection); + const documents = [{ _id: 'test1' }, { _id: 'test2' }]; + await filesCollection.collection.rawCollection().insertMany(documents); + + const result = await cursor.each(); + + expect(result).to.be.an('array'); + result.forEach((fileCursor, index) => { + expect(fileCursor).to.be.instanceOf(FileCursor); + expect(fileCursor._fileRef).to.deep.equal(documents[index]); + }); + }); + }); + + describe('#mapAsync()', function() { + it('should map callback over all matching documents', async function() { + const cursor = new FilesCursor({}, {}, filesCollection); + const documents = [{ _id: 'test1' }, { _id: 'test2' }]; + await filesCollection.collection.rawCollection().insertMany(documents); + + const result = await cursor.mapAsync((doc) => doc._id); + expect(result).to.deep.equal(documents.map((doc) => doc._id)); + }); + }); + + describe('#current()', function() { + it('should return the current item on the cursor', async function() { + const cursor = new FilesCursor({}, {}, filesCollection); + const documents = [{ _id: 'test1' }, { _id: 'test2' }]; + sandbox.stub(cursor, 'fetchAsync').returns(Promise.resolve(documents)); + const current = await cursor.current(); + expect(current).to.deep.equal(documents[0]); + }); + }); + + describe('#observe()', function() { + it('should call observe on the cursor', function() { + const cursor = new FilesCursor({}, {}, filesCollection); + const observeStub = sandbox.stub(cursor.cursor, 'observe'); + const callbacks = {}; + cursor.observe(callbacks); + sinon.assert.calledWith(observeStub, callbacks); + }); + }); + + describe('#observeChanges()', function() { + it('should call observeChanges on the cursor', function() { + const cursor = new FilesCursor({}, {}, filesCollection); + const observeChangesStub = sandbox.stub(cursor.cursor, 'observeChanges'); + const callbacks = {}; + cursor.observeChanges(callbacks); + sinon.assert.calledWith(observeChangesStub, callbacks); + }); + }); +}); diff --git a/tests/server.js b/tests/server.js new file mode 100644 index 0000000..a3402b3 --- /dev/null +++ b/tests/server.js @@ -0,0 +1,4 @@ +import './helpers'; +import './core.test'; +import './cursor.test'; +import './server.test'; diff --git a/tests/server.test.js b/tests/server.test.js new file mode 100644 index 0000000..6979701 --- /dev/null +++ b/tests/server.test.js @@ -0,0 +1,597 @@ +/* global describe, beforeEach, it, before, afterEach, Meteor */ + +import { expect } from 'chai'; +import fs from 'fs'; +import sinon from 'sinon'; +import { FilesCollection } from '../server'; +import http from 'http'; +import {Readable} from 'stream'; + +describe('FilesCollection Constructor', function() { + describe('constructor', function() { + it('should create an instance of FilesCollection', async function() { + const filesCollection = new FilesCollection({ collectionName: 'test123'}); + expect(filesCollection instanceof FilesCollection).to.be.true; + }); + }); +}); + + +describe('FilesCollection', () => { + describe('#_prepareUpload', () => { + let filesCollection; + let opts; + let userId; + let transport; + let namingFunctionStub; + let onBeforeUploadStub; + let onInitiateUploadStub; + + before(() =>{ + filesCollection = new FilesCollection({ collectionName: 'testserver-prepareUpload', namingFunction: () => {}, onBeforeUpload: () => true, onInitiateUpload: () => {}}); + }); + + beforeEach(() => { + opts = { + file: { + name: 'testFile', + meta: {}, + }, + fileId: '123', + }; + userId = 'user1'; + transport = 'http'; + + // Stubbing the namingFunction method + namingFunctionStub = sinon.stub(filesCollection, 'namingFunction'); + namingFunctionStub.returns('newName'); + + // Stubbing the onBeforeUpload method + onBeforeUploadStub = sinon.stub(filesCollection, 'onBeforeUpload'); + onBeforeUploadStub.returns(true); + + // Stubbing the onInitiateUpload method + onInitiateUploadStub = sinon.stub(filesCollection, 'onInitiateUpload'); + }); + + afterEach(() => { + // Restore the stubbed methods after each test + sinon.restore(); + }); + + it('should prepare upload successfully', async () => { + const { result, opts: newOpts } = await filesCollection._prepareUpload(opts, userId, transport); + + expect(result).to.be.an('object'); + expect(newOpts).to.be.an('object'); + expect(namingFunctionStub.calledOnce).to.be.true; + expect(onBeforeUploadStub.calledOnce).to.be.true; + expect(onInitiateUploadStub.called).to.be.false; + }); + }); + + describe('#_finishUpload', () => { + let filesCollection; + let result; + let opts; + let cb; + let chmodStub; + let insertAsyncStub; + let updateAsyncStub; + let onAfterUploadSpy; + + before(() => { + filesCollection = new FilesCollection({ collectionName: 'testserver-finishUpload'}); + }); + + beforeEach(() => { + result = { path: '/path/to/file' }; + opts = { file: { name: 'testFile', meta: {} } }; + cb = sinon.stub(); + + // Stubbing the fs.chmod method + chmodStub = sinon.stub(fs, 'chmod'); + chmodStub.callsFake((path, permissions, callback) => callback()); + + // Stubbing the collection.insert method + insertAsyncStub = sinon.stub(filesCollection.collection, 'insertAsync'); + + // Stubbing the _preCollection.update method + updateAsyncStub = sinon.stub(filesCollection._preCollection, 'updateAsync'); + + // Creating a spy for the onAfterUpload hook + onAfterUploadSpy = sinon.spy(); + filesCollection.onAfterUpload = onAfterUploadSpy; + }); + + afterEach(() => { + // Restore the stubbed methods after each test + sinon.restore(); + }); + + it('should finish upload successfully', async () => { + await filesCollection._finishUpload(result, opts, cb); + + expect(chmodStub.calledOnce).to.be.true; + expect(insertAsyncStub.calledOnce).to.be.true; + expect(updateAsyncStub.calledOnce).to.be.true; + expect(cb.calledOnce).to.be.true; + expect(cb.calledWith(null, result)).to.be.true; + }); + + it('should call callback with a single error argument if insert fails', async () => { + const error = new Meteor.Error(500, 'Insert failed'); + insertAsyncStub.throws(error); + + await filesCollection._finishUpload(result, opts, cb); + + expect(chmodStub.calledOnce).to.be.true; + expect(insertAsyncStub.calledOnce).to.be.true; + expect(updateAsyncStub.called).to.be.false; + expect(cb.calledOnce).to.be.true; + expect(cb.calledWith(error)).to.be.true; + }); + + it('should call callback with a single error argument if update fails', async () => { + const error = new Meteor.Error(500, 'Update failed'); + updateAsyncStub.throws(error); + + await filesCollection._finishUpload(result, opts, cb); + + expect(chmodStub.calledOnce).to.be.true; + expect(insertAsyncStub.calledOnce).to.be.true; + expect(updateAsyncStub.calledOnce).to.be.true; + expect(cb.calledOnce).to.be.true; + expect(cb.calledWith(error)).to.be.true; + }); + + it('should call onAfterUpload hook if it is a function', async () => { + await filesCollection._finishUpload(result, opts, cb); + + expect(onAfterUploadSpy.calledOnce).to.be.true; + expect(onAfterUploadSpy.calledWith(result)).to.be.true; + }); + }); + + describe('#write()', function() { + let filesCollection; let collectionMock; + let fsPromiseStatStub; let fsPromisesMkdirStub; let fsPromiseWriteFileStub; let fsCreateWriteStreamStub; + + before(function() { + filesCollection = new FilesCollection({ collectionName: 'testserver-writeAsync'}); + }); + + beforeEach(function() { + fsPromiseStatStub = sinon.stub(fs.promises, 'stat'); + fsPromisesMkdirStub = sinon.stub(fs.promises, 'mkdir'); + fsPromiseWriteFileStub = sinon.stub(fs.promises, 'writeFile'); + fsCreateWriteStreamStub = sinon.stub(fs, 'createWriteStream'); + collectionMock = sinon.mock(filesCollection.collection); + }); + + afterEach(async function() { + fsPromiseStatStub.restore(); + fsPromisesMkdirStub.restore(); + fsPromiseWriteFileStub.restore(); + fsCreateWriteStreamStub.restore(); + collectionMock.restore(); + await filesCollection.collection.removeAsync({}); + }); + + it('should write buffer to FS and add to FilesCollection Collection', async function() { + const buffer = Buffer.from('test data'); + const opts = { name: 'test.txt', type: 'text/plain', meta: {}, userId: 'user1', fileId: 'file1' }; + + fsPromiseStatStub.resolves({ isFile: () => true }); + fsPromiseWriteFileStub.resolves(); + fsCreateWriteStreamStub.returns({ + end: (_buffer, cb) => cb(null) + }); + + collectionMock.expects('insertAsync').resolves('file1'); + collectionMock.expects('findOneAsync').resolves({ _id: 'file1' }); + + const result = await filesCollection.write(buffer, opts, true); + + collectionMock.verify(); + expect(result).to.be.an('object'); + expect(result).to.have.property('_id', 'file1'); + }); + + it('should make all directories if not present, then write buffer to FS and then add to FilesCollection Collection', async function() { + const buffer = Buffer.from('test data'); + const opts = { name: 'test.txt', type: 'text/plain', meta: {}, userId: 'user1', fileId: 'file1' }; + + fsPromiseStatStub.resolves({ isFile: () => true }); + fsPromiseWriteFileStub.resolves(); + fsCreateWriteStreamStub.returns({ + end: (_buffer, cb) => cb(null) + }); + + collectionMock.expects('insertAsync').resolves('file1'); + collectionMock.expects('findOneAsync').resolves({ _id: 'file1' }); + + const result = await filesCollection.write(buffer, opts, true); + + collectionMock.verify(); + expect(result).to.be.an('object'); + expect(result).to.have.property('_id', 'file1'); + }); + + it('should call callback with error if file could not be written to FS', function(done) { + const buffer = Buffer.from('test data'); + const opts = { name: 'test.txt', type: 'text/plain', meta: {}, userId: 'user1', fileId: 'file1' }; + + fsPromiseStatStub.resolves({ isFile: () => true }); + fsCreateWriteStreamStub.returns({ + end: (_buffer, cb) => cb(new Error('Test Error')) + }); + + filesCollection.write(buffer, opts, true).catch((e) => { + expect(e).to.be.instanceOf(Error); + done(); + }); + }); + + it('should call callback with error if file could not be added to FilesCollection Collection', function(done) { + const buffer = Buffer.from('test data'); + const opts = { name: 'test.txt', type: 'text/plain', meta: {}, userId: 'user1', fileId: 'file1' }; + + fsPromiseStatStub.resolves({ isFile: () => true }); + fsCreateWriteStreamStub.returns({ + end: (_buffer, cb) => cb(null) + }); + + collectionMock.expects('insertAsync').rejects(new Error('Test Error')); + + filesCollection.write(buffer, opts, true).catch((e) => { + expect(e).to.be.instanceOf(Error); + done(); + }); + }); + + it('actually writes the file to the FS and to the db (no mocking)', async function() { + const testData = 'test data'; + const buffer = Buffer.from(testData); + + const opts = { path: '/tmp/test.txt', name: 'test.txt', type: 'text/plain', meta: {}, userId: 'user1', fileId: 'file1' }; + + sinon.stub(filesCollection, 'storagePath').returns('/tmp'); + fsPromiseStatStub.restore(); + fsPromiseWriteFileStub.restore(); + fsCreateWriteStreamStub.restore(); + fsPromisesMkdirStub.restore(); + + const result = await filesCollection.write(buffer, opts, true); + + const file = await filesCollection.collection.findOneAsync({ _id: 'file1' }); + expect(file).to.be.an('object'); + expect(file).to.have.property('_id', 'file1'); + expect(file).to.have.property('name', 'test.txt'); + expect(file).to.have.property('size', 9); + expect(file).to.have.property('type', 'text/plain'); + expect(file).to.have.property('extension', 'txt'); + expect(file).to.have.property('path'); + expect(file).to.have.property('versions'); + expect(file.versions).to.have.property('original'); + expect(file.versions.original).to.have.property('path'); + expect(file.versions.original).to.have.property('size', 9); + expect(file.versions.original).to.have.property('type', 'text/plain'); + expect(file.versions.original).to.have.property('extension', 'txt'); + + const fileOnDisk = fs.readFileSync(file.versions.original.path, 'utf8'); + expect(fileOnDisk).to.deep.equal(testData); + + expect(result).to.be.an('object'); + expect(result).to.have.property('_id', 'file1'); + + // Cleanup∏ + await fs.promises.unlink(file.versions.original.path); + }); + }); + + describe('#load()', function() { + let filesCollection; + const testdata = 'test data'; + let port; + + before(function() { + filesCollection = new FilesCollection({ collectionName: 'testserver-loadAsync'}); + + const server = http.createServer((req, res) => { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + res.end(testdata); + }); + + server.listen(undefined, '127.0.0.1', () => { + port = server.address().port; + }); + }); + + beforeEach(function() { + }); + + afterEach(async function() { + await filesCollection.collection.removeAsync({}); + }); + + it('should download file over HTTP, write stream to FS, and add to FilesCollection Collection', async function() { + const url = 'http://127.0.0.1:' + port; + const opts = { name: 'test.txt', type: 'text/plain', meta: {}, userId: 'user1', fileId: 'file1', timeout: 360000 }; + + const result = await filesCollection.load(url, opts, true); + + expect(result).to.be.an('object'); + + const file = await filesCollection.collection.findOneAsync({ _id: result._id }); + expect(file).to.be.an('object'); + }); + }); + + describe('#addFileAsync', () => { + let filesCollection; + let path; + let opts; + let proceedAfterUpload; + + before(() => { + filesCollection = new FilesCollection({ collectionName: 'testserver', onAfterUpload: () => {}}); + path = '/tmp/meteor-test-file.txt'; + fs.writeFileSync(path, 'test'); + opts = { type: 'text/plain'}; + proceedAfterUpload = false; + }); + + afterEach(() => { + // Restore the stubbed methods after each test + sinon.restore(); + }); + + it('should add a file successfully', async () => { + const result = await filesCollection.addFile(path, opts, proceedAfterUpload); + + // Check if the result is correct + expect(result).to.be.an('object'); + expect(result).to.have.property('_id'); + expect(result).to.have.property('name', 'meteor-test-file.txt'); + expect(result).to.have.property('size', 4); + expect(result).to.have.property('type', 'text/plain'); + expect(result).to.have.property('extension', 'txt'); + expect(result).to.have.property('extensionWithDot', '.txt'); + expect(result).to.have.property('path', path); + + // Check if the file exists in the database + const file = await filesCollection.collection.findOneAsync({ _id: result._id }); + expect(file).to.be.an('object'); + expect(file).to.have.property('_id'); + expect(file).to.have.property('name', 'meteor-test-file.txt'); + expect(file).to.have.property('size', 4); + expect(file).to.have.property('type', 'text/plain'); + expect(file).to.have.property('extension', 'txt'); + expect(file).to.have.property('extensionWithDot', '.txt'); + expect(file).to.have.property('path', path); + }); + + it('should call onAfterUpload hook if flag is true', async () => { + // Stub the `onAfterUpload` method + sinon.stub(filesCollection, 'onAfterUpload'); + + await filesCollection.addFile(path, opts, true); + + expect(filesCollection.onAfterUpload.calledWith()).to.be.true; + }); + + it('should not call onAfterUpload hook if flag is false', async () => { + // Stub the `onAfterUpload` method + sinon.stub(filesCollection, 'onAfterUpload'); + + await filesCollection.addFile(path, opts, false); + + expect(filesCollection.onAfterUpload.calledWith()).to.be.false; + }); + + it('should throw an error if file does not exist', async () => { + const nonExistingPath = '/tmp/meteor-test-file-non-existing.txt'; + try { + await filesCollection.addFile(nonExistingPath, opts, proceedAfterUpload); + } catch (e) { + expect(e).to.be.instanceOf(Meteor.Error); + expect(e.error).to.equal(400); + } + } + ); + + it('should throw an error if file is not readable', async () => { + const nonReadablePath = '/tmp/meteor-test-file-non-readable.txt'; + fs.writeFileSync(nonReadablePath, 'test'); + fs.chmodSync(nonReadablePath, 0o200); + try { + await filesCollection.addFile(nonReadablePath, opts, proceedAfterUpload); + } catch (e) { + expect(e).to.be.instanceOf(Meteor.Error); + expect(e.error).to.equal(400); + } + }); + + it('should throw an error, if path is not a file', async () => { + const nonFile = '/tmp'; + try { + await filesCollection.addFile(nonFile, opts, proceedAfterUpload); + } catch (e) { + expect(e).to.be.instanceOf(Meteor.Error); + expect(e.error).to.equal(400); + } + }); + + it('should throw an error, if file is added to a public collection', async () => { + const publicFilesCollection = new FilesCollection({ collectionName: 'testserver-pub', public: true, storagePath: '/tmp', downloadRoute: '/public' }); + try { + await publicFilesCollection.addFile(path, opts, proceedAfterUpload); + } catch (e) { + expect(e).to.be.instanceOf(Meteor.Error); + expect(e.error).to.equal(403); + } + }); + }); + + describe('#download', () => { + let filesCollection; + let httpObj; + let version; + let fileRef; + let statStub; + let _404Stub; + let serveStub; + + before(() => { + filesCollection = new FilesCollection({collectionName: 'testserver-downloadAsync', downloadCallbackAsync: async () => {return true;}}); + }); + + beforeEach(() => { + httpObj = { request: { originalUrl: '/path/to/file', headers: { 'x-mtok': 'token'} }, response: { writeHead: () => {}, end: () => {}} }; + + version = 'original'; + fileRef = { + versions: { + original: { + path: '/path/to/file', + size: 100, + }, + }, + }; + + // Stubbing the fs.promises.stat method + statStub = sinon.stub(fs.promises, 'stat'); + statStub.resolves({ isFile: () => true, size: 100 }); + + // Stubbing the _404 method + _404Stub = sinon.stub(filesCollection, '_404'); + + // Stubbing the serve method + serveStub = sinon.stub(filesCollection, 'serve'); + }); + + afterEach(() => { + // Restore the stubbed methods after each test + sinon.restore(); + }); + + it('should download a file successfully', async () => { + await filesCollection.download(httpObj, version, fileRef); + + expect(statStub.calledOnce).to.be.true; + expect(_404Stub.called).to.be.false; + expect(serveStub.calledOnce).to.be.true; + }); + + it('should return 404 if file does not exist', async () => { + statStub.resolves({ isFile: () => false }); + await filesCollection.download(httpObj, version, fileRef); + + expect(statStub.calledOnce).to.be.true; + expect(_404Stub.calledOnce).to.be.true; + expect(serveStub.called).to.be.false; + }); + + it('should call downloadCallbackAsync if it is a function', async () => { + const downloadCallback = sinon.stub().returns(new Promise((resolve) => resolve(true))); + filesCollection.downloadCallback = downloadCallback; + await filesCollection.download(httpObj, version, fileRef); + + expect(statStub.calledOnce).to.be.true; + expect(_404Stub.called).to.be.false; + expect(serveStub.calledOnce).to.be.true; + expect(downloadCallback.calledOnce).to.be.true; + }); + + it('should not call downloadCallback if it is not a function', async () => { + filesCollection.downloadCallbackAsync = null; + await filesCollection.download(httpObj, version, fileRef); + + expect(statStub.calledOnce).to.be.true; + expect(_404Stub.called).to.be.false; + expect(serveStub.calledOnce).to.be.true; + }); + + it('should return 404 if downloadCallbackAsync returns false', async () => { + const downloadCallback = sinon.stub().returns(new Promise((resolve) => resolve(false))); + filesCollection.downloadCallback = downloadCallback; + await filesCollection.download(httpObj, version, fileRef); + + expect(statStub.calledOnce).to.be.false; + expect(_404Stub.calledOnce).to.be.true; + expect(serveStub.called).to.be.false; + + filesCollection.downloadCallback = null; + }); + + it('should call interceptDownload if it is a function, that resolves true', async () => { + const interceptDownload = sinon.stub().resolves(true); + filesCollection.interceptDownload = interceptDownload; + await filesCollection.download(httpObj, version, fileRef); + + expect(statStub.calledOnce).to.be.false; + expect(_404Stub.called).to.be.false; + expect(serveStub.calledOnce).to.be.false; + expect(interceptDownload.calledOnce).to.be.true; + + filesCollection.interceptDownload = null; + }); + + it('should proceed if interceptDownload is a function, that returns false', async () => { + const interceptDownload = sinon.stub().resolves(false); + filesCollection.interceptDownload = interceptDownload; + await filesCollection.download(httpObj, version, fileRef); + + expect(statStub.calledOnce).to.be.true; + expect(_404Stub.called).to.be.false; + expect(serveStub.calledOnce).to.be.true; + expect(interceptDownload.calledOnce).to.be.true; + + filesCollection.interceptDownload = null; + }); + }); + + describe('#serve', function() { + let server; + let filesCollection; + let port; + + before(function() { + filesCollection = new FilesCollection({ collectionName: 'testserver-serve' }); + }); + + beforeEach(async function() { + const path = '/tmp/testfile.txt'; + const content = 'testfile'; + server = http.createServer((req, res) => { + const readableStream = Readable.from(content); + const fileRef = { name: 'testfile.txt' }; + const vRef = { name: 'testfile.txt', size: Buffer.byteLength(content), path }; + + filesCollection.serve({request: req, response: res}, fileRef, vRef, 'original', readableStream); + }); + server.listen(0); + + port = server.address().port; + }); + + afterEach(function() { + server.close(); + }); + + it('should serve a fileRef object', function(done) { + http.get('http://localhost:' + port, (res) => { + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => { + expect(data).to.equal('testfile'); + done(); + }); + }); + }); + }); +}); diff --git a/upload.js b/upload.js index 6ecee3d..8565d5f 100644 --- a/upload.js +++ b/upload.js @@ -670,64 +670,73 @@ export class UploadInstance extends EventEmitter { return this; } - start() { + async start() { let isUploadAllowed; if (this.fileData.size <= 0) { this.end(new Meteor.Error(400, 'Can\'t upload empty file')); return this.result; } - if (this.config.onBeforeUpload && helpers.isFunction(this.config.onBeforeUpload)) { - isUploadAllowed = this.config.onBeforeUpload.call(Object.assign({}, this.result, this.collection._getUser()), this.fileData); - if (isUploadAllowed !== true) { - return this.end(new Meteor.Error(403, helpers.isString(isUploadAllowed) ? isUploadAllowed : 'config.onBeforeUpload() returned false')); + try { + if (this.config.onBeforeUpload && helpers.isFunction(this.config.onBeforeUpload)) { + isUploadAllowed = await Promise.resolve(this.config.onBeforeUpload.call(Object.assign({}, this.result, this.collection._getUser()), this.fileData)); + if (isUploadAllowed !== true) { + return this.end(new Meteor.Error(403, helpers.isString(isUploadAllowed) ? isUploadAllowed : 'config.onBeforeUpload() returned false')); + } } - } - if (this.collection.onBeforeUpload && helpers.isFunction(this.collection.onBeforeUpload)) { - isUploadAllowed = this.collection.onBeforeUpload.call(Object.assign({}, this.result, this.collection._getUser()), this.fileData); - if (isUploadAllowed !== true) { - return this.end(new Meteor.Error(403, helpers.isString(isUploadAllowed) ? isUploadAllowed : 'collection.onBeforeUpload() returned false')); - } - } - - Tracker.autorun((computation) => { - this.trackerComp = computation; - if (!this.result.onPause.curValue && !Meteor.status().connected) { - this.collection._debug('[FilesCollection] [insert] [Tracker] [pause]'); - this.result.pause(); - } else if (this.result.onPause.curValue && Meteor.status().connected) { - this.collection._debug('[FilesCollection] [insert] [Tracker] [continue]'); - this.result.continue(); + if (this.collection.onBeforeUpload && helpers.isFunction(this.collection.onBeforeUpload)) { + isUploadAllowed = await Promise.resolve(this.collection.onBeforeUpload.call(Object.assign({}, this.result, this.collection._getUser()), this.fileData)); + if (isUploadAllowed !== true) { + return this.end(new Meteor.Error(403, helpers.isString(isUploadAllowed) ? isUploadAllowed : 'collection.onBeforeUpload() returned false')); + } } - }); - if (this.worker) { - this.collection._debug('[FilesCollection] [insert] using WebWorkers'); - this.worker.onmessage = (evt) => { - if (evt.data.error) { - this.collection._debug('[FilesCollection] [insert] [worker] [onmessage] [ERROR:]', evt.data.error); - this.emit('proceedChunk', evt.data.chunkId); - } else { - this.emit('sendChunk', evt); + Tracker.autorun((computation) => { + this.trackerComp = computation; + if (!this.result.onPause.curValue && !Meteor.status().connected) { + this.collection._debug('[FilesCollection] [insert] [Tracker] [pause]'); + this.result.pause(); + } else if (this.result.onPause.curValue && Meteor.status().connected) { + this.collection._debug('[FilesCollection] [insert] [Tracker] [continue]'); + this.result.continue(); } - }; + }); - this.worker.onerror = (e) => { - this.collection._debug('[FilesCollection] [insert] [worker] [onerror] [ERROR:]', e); - this.emit('end', e.message); - }; - } else { - this.collection._debug('[FilesCollection] [insert] using MainThread'); - } + if (this.worker) { + this.collection._debug('[FilesCollection] [insert] using WebWorkers'); + this.worker.onmessage = (evt) => { + if (evt.data.error) { + this.collection._debug('[FilesCollection] [insert] [worker] [onmessage] [ERROR:]', evt.data.error); + this.emit('proceedChunk', evt.data.chunkId); + } else { + this.emit('sendChunk', evt); + } + }; - this.emit('prepare'); - return this.result; + this.worker.onerror = (e) => { + this.collection._debug('[FilesCollection] [insert] [worker] [onerror] [ERROR:]', e); + this.emit('end', e.message); + }; + } else { + this.collection._debug('[FilesCollection] [insert] using MainThread'); + } + + this.emit('prepare'); + return this.result; + } catch (error) { + return this.end(new Meteor.Error(500, `Error in onBeforeUpload: ${error.message}`)); + } } manual() { - this.result.start = () => { - this.emit('start'); + this.result.start = async () => { + try { + await this.start(); + this.emit('start'); + } catch (error) { + this.emit('end', error); + } }; const self = this;