From aeba3603cc0eb2a2d0c35bd55717b1a24cd566eb Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Tue, 24 Mar 2026 07:05:01 -0500 Subject: [PATCH 1/5] chore: add oxlint/oxfmt migration scaffolding --- .oxfmtrc.json | 19 ++ .oxlintrc.json | 29 ++ README.md | 8 +- package.json | 10 +- packages/api-framework/package.json | 5 +- packages/bookshelf-collision/package.json | 5 +- packages/bookshelf-custom-query/package.json | 5 +- packages/bookshelf-eager-load/package.json | 5 +- packages/bookshelf-filter/package.json | 5 +- packages/bookshelf-has-posts/package.json | 5 +- packages/bookshelf-include-count/package.json | 5 +- packages/bookshelf-order/package.json | 5 +- packages/bookshelf-pagination/package.json | 5 +- packages/bookshelf-plugins/package.json | 5 +- packages/bookshelf-search/package.json | 5 +- .../bookshelf-transaction-events/package.json | 5 +- packages/config/package.json | 5 +- packages/database-info/package.json | 5 +- packages/debug/package.json | 5 +- packages/domain-events/package.json | 5 +- packages/elasticsearch/package.json | 5 +- packages/email-mock-receiver/package.json | 5 +- packages/errors/package.json | 5 +- packages/express-test/package.json | 5 +- packages/http-cache-utils/package.json | 5 +- packages/http-stream/package.json | 5 +- packages/jest-snapshot/package.json | 5 +- packages/job-manager/package.json | 5 +- packages/logging/package.json | 5 +- packages/metrics/package.json | 5 +- packages/mw-error-handler/package.json | 5 +- packages/mw-vhost/package.json | 5 +- packages/nodemailer/package.json | 5 +- packages/pretty-cli/package.json | 5 +- packages/pretty-stream/package.json | 5 +- packages/prometheus-metrics/package.json | 5 +- packages/promise/package.json | 5 +- packages/request/package.json | 5 +- packages/root-utils/package.json | 5 +- packages/security/package.json | 5 +- packages/server/package.json | 5 +- packages/tpl/package.json | 5 +- packages/validator/package.json | 5 +- packages/version/package.json | 5 +- packages/webhook-mock-receiver/package.json | 5 +- packages/zip/package.json | 5 +- yarn.lock | 247 ++++++++++++++++++ 47 files changed, 435 insertions(+), 88 deletions(-) create mode 100644 .oxfmtrc.json create mode 100644 .oxlintrc.json diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 000000000..76ea23648 --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,19 @@ +{ + "tabWidth": 4, + "singleQuote": true, + "semi": true, + "trailingComma": "none", + "quoteProps": "as-needed", + "bracketSpacing": false, + "arrowParens": "avoid", + "ignorePatterns": [ + "**/node_modules/**", + "**/build/**", + "**/coverage/**", + "**/cjs/**", + "**/es/**", + "**/types/**", + "**/*.hbs", + "**/test/fixtures/**" + ] +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 000000000..5a03d54b0 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,29 @@ +{ + "plugins": ["node", "typescript"], + "rules": { + "eqeqeq": ["error", "always"], + "no-plusplus": ["error", {"allowForLoopAfterthoughts": true}], + "no-eval": "error", + "no-useless-call": "error", + "no-console": "error", + "no-shadow": "error", + "array-callback-return": "error", + "no-constructor-return": "error", + "no-promise-executor-return": "error", + "dot-notation": "error", + "no-var": "warn", + "no-unused-vars": "off", + "no-unused-expressions": "off", + "no-unused-private-class-members": "off", + "unicorn/no-thenable": "off" + }, + "ignorePatterns": [ + "**/node_modules/**", + "**/build/**", + "**/coverage/**", + "**/cjs/**", + "**/es/**", + "**/types/**", + "**/test/fixtures/**" + ] +} diff --git a/README.md b/README.md index 28b1be74b..136bcad43 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,12 @@ To add a new package to the repo: ## Test -- `yarn lint` run just eslint -- `yarn test` run lint and tests +- `yarn lint` run `oxlint` first, then existing ESLint checks +- `yarn lint:oxlint` run `oxlint` only +- `yarn lint:eslint` run ESLint-only compatibility checks +- `yarn format` format `js/ts/json/md` files with `oxfmt` +- `yarn format:check` check `js/ts/json/md` formatting with `oxfmt` +- `yarn test` run tests (many packages still run lint in `posttest`) ## Publish diff --git a/package.json b/package.json index aed52fe7d..5c53c8327 100644 --- a/package.json +++ b/package.json @@ -19,13 +19,17 @@ "setup": "yarn", "test": "nx run-many -t test --parallel=10 --outputStyle=dynamic-legacy", "test:ci": "nx run-many -t test --outputStyle=dynamic", - "lint": "nx run-many -t lint --outputStyle=dynamic", + "lint": "yarn lint:oxlint && yarn lint:eslint", "preship": "git diff --quiet && git diff --cached --quiet || (echo 'Error: working tree must be clean before shipping' && exit 1) && yarn test", "ship": "nx release version --git-push --git-remote ${GHOST_UPSTREAM:-origin}", "ship:patch": "yarn ship patch", "ship:minor": "yarn ship minor", "ship:major": "yarn ship major", - "ship:first-release": "yarn ship patch --first-release" + "ship:first-release": "yarn ship patch --first-release", + "lint:eslint": "nx run-many -t lint:eslint --outputStyle=dynamic", + "lint:oxlint": "oxlint -c .oxlintrc.json packages", + "format": "oxfmt -c .oxfmtrc.json \"packages/**/*.{js,ts,json,md}\"", + "format:check": "oxfmt -c .oxfmtrc.json --check \"packages/**/*.{js,ts,json,md}\"" }, "devDependencies": { "@nx/js": "22.6.1", @@ -33,6 +37,8 @@ "eslint": "8.57.1", "eslint-plugin-ghost": "3.5.0", "nx": "22.6.1", + "oxfmt": "0.41.0", + "oxlint": "1.56.0", "sinon": "21.0.3", "ts-node": "10.9.2", "vitest": "3.2.4" diff --git a/packages/api-framework/package.json b/packages/api-framework/package.json index 265b5a428..49823b5a1 100644 --- a/packages/api-framework/package.json +++ b/packages/api-framework/package.json @@ -16,8 +16,9 @@ "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "test": "yarn test:unit", "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "yarn lint:code && yarn lint:test", - "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", + "lint:eslint": "yarn lint:code && yarn lint:test" }, "files": [ "index.js", diff --git a/packages/bookshelf-collision/package.json b/packages/bookshelf-collision/package.json index 3f5887957..3f34fb02e 100644 --- a/packages/bookshelf-collision/package.json +++ b/packages/bookshelf-collision/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/bookshelf-custom-query/package.json b/packages/bookshelf-custom-query/package.json index 7d94fc600..059404c76 100644 --- a/packages/bookshelf-custom-query/package.json +++ b/packages/bookshelf-custom-query/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/bookshelf-eager-load/package.json b/packages/bookshelf-eager-load/package.json index f36d62599..d534b288b 100644 --- a/packages/bookshelf-eager-load/package.json +++ b/packages/bookshelf-eager-load/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/bookshelf-filter/package.json b/packages/bookshelf-filter/package.json index b0f598663..a61e43da0 100644 --- a/packages/bookshelf-filter/package.json +++ b/packages/bookshelf-filter/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/bookshelf-has-posts/package.json b/packages/bookshelf-has-posts/package.json index 2132b93bc..276807e7b 100644 --- a/packages/bookshelf-has-posts/package.json +++ b/packages/bookshelf-has-posts/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/bookshelf-include-count/package.json b/packages/bookshelf-include-count/package.json index 0d7c564c8..21b717c68 100644 --- a/packages/bookshelf-include-count/package.json +++ b/packages/bookshelf-include-count/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/bookshelf-order/package.json b/packages/bookshelf-order/package.json index 3a4635151..0eaae94b6 100644 --- a/packages/bookshelf-order/package.json +++ b/packages/bookshelf-order/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/bookshelf-pagination/package.json b/packages/bookshelf-pagination/package.json index 178c1b1ef..6d9270bdd 100644 --- a/packages/bookshelf-pagination/package.json +++ b/packages/bookshelf-pagination/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/bookshelf-plugins/package.json b/packages/bookshelf-plugins/package.json index d23c562cc..9dc759d09 100644 --- a/packages/bookshelf-plugins/package.json +++ b/packages/bookshelf-plugins/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/bookshelf-search/package.json b/packages/bookshelf-search/package.json index 0cb6b009e..3cbcd077b 100644 --- a/packages/bookshelf-search/package.json +++ b/packages/bookshelf-search/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/bookshelf-transaction-events/package.json b/packages/bookshelf-transaction-events/package.json index 9461a0103..f006b0f71 100644 --- a/packages/bookshelf-transaction-events/package.json +++ b/packages/bookshelf-transaction-events/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/config/package.json b/packages/config/package.json index f2e8d1407..7e9856ec2 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/database-info/package.json b/packages/database-info/package.json index 67c925070..e8e7804ab 100644 --- a/packages/database-info/package.json +++ b/packages/database-info/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/debug/package.json b/packages/debug/package.json index 183885908..81ad04a39 100644 --- a/packages/debug/package.json +++ b/packages/debug/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/domain-events/package.json b/packages/domain-events/package.json index a653afdca..be94637fb 100644 --- a/packages/domain-events/package.json +++ b/packages/domain-events/package.json @@ -13,8 +13,9 @@ "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "test": "yarn test:unit", "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "yarn lint:code && yarn lint:test", - "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", + "lint:eslint": "yarn lint:code && yarn lint:test" }, "files": [ "index.js", diff --git a/packages/elasticsearch/package.json b/packages/elasticsearch/package.json index f1ec48021..e69610b27 100644 --- a/packages/elasticsearch/package.json +++ b/packages/elasticsearch/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/email-mock-receiver/package.json b/packages/email-mock-receiver/package.json index 9d6540e1f..52b68f5c9 100644 --- a/packages/email-mock-receiver/package.json +++ b/packages/email-mock-receiver/package.json @@ -14,8 +14,9 @@ "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "test": "yarn test:unit", "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "yarn lint:code && yarn lint:test", - "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", + "lint:eslint": "yarn lint:code && yarn lint:test" }, "files": [ "index.js", diff --git a/packages/errors/package.json b/packages/errors/package.json index 5ba035ff1..99029e645 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -22,8 +22,9 @@ "build:es": "esbuild src/*.ts --target=es2020 --outdir=es --format=esm", "build:types": "tsc --emitDeclarationOnly --declaration --declarationMap --outDir types", "test": "NODE_ENV=testing vitest run --coverage", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "cjs", diff --git a/packages/express-test/package.json b/packages/express-test/package.json index 33af91b69..0028e8c18 100644 --- a/packages/express-test/package.json +++ b/packages/express-test/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/http-cache-utils/package.json b/packages/http-cache-utils/package.json index f89694b6e..c6bdf9590 100644 --- a/packages/http-cache-utils/package.json +++ b/packages/http-cache-utils/package.json @@ -14,8 +14,9 @@ "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "test": "yarn test:unit", "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "yarn lint:code && yarn lint:test", - "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", + "lint:eslint": "yarn lint:code && yarn lint:test" }, "files": [ "index.js", diff --git a/packages/http-stream/package.json b/packages/http-stream/package.json index 6761f6625..006bf0342 100644 --- a/packages/http-stream/package.json +++ b/packages/http-stream/package.json @@ -13,9 +13,10 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "yarn lint:code && yarn lint:test", + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", - "posttest": "yarn lint" + "posttest": "yarn lint", + "lint:eslint": "yarn lint:code && yarn lint:test" }, "files": [ "index.js", diff --git a/packages/jest-snapshot/package.json b/packages/jest-snapshot/package.json index 2651952f2..39766bfa9 100644 --- a/packages/jest-snapshot/package.json +++ b/packages/jest-snapshot/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/job-manager/package.json b/packages/job-manager/package.json index a5e4b53ae..6ba548616 100644 --- a/packages/job-manager/package.json +++ b/packages/job-manager/package.json @@ -12,8 +12,9 @@ "test:unit": "NODE_ENV=testing vitest run --coverage", "test": "yarn test:unit", "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "yarn lint:code && yarn lint:test", - "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", + "lint:eslint": "yarn lint:code && yarn lint:test" }, "files": [ "index.js", diff --git a/packages/logging/package.json b/packages/logging/package.json index ce36c0aa1..bec497e5e 100644 --- a/packages/logging/package.json +++ b/packages/logging/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/metrics/package.json b/packages/metrics/package.json index f144eccc0..5857bcf1a 100644 --- a/packages/metrics/package.json +++ b/packages/metrics/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/mw-error-handler/package.json b/packages/mw-error-handler/package.json index 91174984c..9736827f1 100644 --- a/packages/mw-error-handler/package.json +++ b/packages/mw-error-handler/package.json @@ -13,8 +13,9 @@ "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "test": "yarn test:unit", "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "yarn lint:code && yarn lint:test", - "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", + "lint:eslint": "yarn lint:code && yarn lint:test" }, "files": [ "index.js", diff --git a/packages/mw-vhost/package.json b/packages/mw-vhost/package.json index 9f0669c8d..64dd9fbb6 100644 --- a/packages/mw-vhost/package.json +++ b/packages/mw-vhost/package.json @@ -12,8 +12,9 @@ "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "test": "yarn test:unit", "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "yarn lint:code && yarn lint:test", - "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", + "lint:eslint": "yarn lint:code && yarn lint:test" }, "files": [ "index.js", diff --git a/packages/nodemailer/package.json b/packages/nodemailer/package.json index cdf792065..4d0607ece 100644 --- a/packages/nodemailer/package.json +++ b/packages/nodemailer/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/pretty-cli/package.json b/packages/pretty-cli/package.json index 8815a8cd2..72c0adc7b 100644 --- a/packages/pretty-cli/package.json +++ b/packages/pretty-cli/package.json @@ -13,8 +13,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/pretty-stream/package.json b/packages/pretty-stream/package.json index e9e87003f..d7864b026 100644 --- a/packages/pretty-stream/package.json +++ b/packages/pretty-stream/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/prometheus-metrics/package.json b/packages/prometheus-metrics/package.json index 045c9486e..6719af3b4 100644 --- a/packages/prometheus-metrics/package.json +++ b/packages/prometheus-metrics/package.json @@ -21,8 +21,9 @@ "test": "yarn test:types && yarn test:unit", "test:types": "tsc --noEmit", "lint:code": "eslint src/ --ext .ts --cache", - "lint": "yarn lint:code && yarn lint:test", - "lint:test": "eslint -c test/.eslintrc.js test/ --ext .ts --cache" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint:test": "eslint -c test/.eslintrc.js test/ --ext .ts --cache", + "lint:eslint": "yarn lint:code && yarn lint:test" }, "files": [ "build" diff --git a/packages/promise/package.json b/packages/promise/package.json index 4fd4233b6..bc17d4222 100644 --- a/packages/promise/package.json +++ b/packages/promise/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/request/package.json b/packages/request/package.json index 4d958aa5a..f5783279c 100644 --- a/packages/request/package.json +++ b/packages/request/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/root-utils/package.json b/packages/root-utils/package.json index 4cdac628d..6ac28b218 100644 --- a/packages/root-utils/package.json +++ b/packages/root-utils/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/security/package.json b/packages/security/package.json index 8ae6811ff..55f6550cb 100644 --- a/packages/security/package.json +++ b/packages/security/package.json @@ -18,8 +18,9 @@ "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "test": "yarn test:unit", "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "yarn lint:code && yarn lint:test", - "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", + "lint:eslint": "yarn lint:code && yarn lint:test" }, "files": [ "index.js", diff --git a/packages/server/package.json b/packages/server/package.json index 8a6c77ced..1ead50c40 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/tpl/package.json b/packages/tpl/package.json index 233071e4f..43cf38558 100644 --- a/packages/tpl/package.json +++ b/packages/tpl/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/validator/package.json b/packages/validator/package.json index 64ed20aa7..51be92332 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/version/package.json b/packages/version/package.json index 353af966b..9c0d7dbc6 100644 --- a/packages/version/package.json +++ b/packages/version/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/packages/webhook-mock-receiver/package.json b/packages/webhook-mock-receiver/package.json index 7c0ad1529..d8416e04c 100644 --- a/packages/webhook-mock-receiver/package.json +++ b/packages/webhook-mock-receiver/package.json @@ -13,9 +13,10 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "yarn lint:code && yarn lint:test", + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", - "posttest": "yarn lint" + "posttest": "yarn lint", + "lint:eslint": "yarn lint:code && yarn lint:test" }, "files": [ "index.js", diff --git a/packages/zip/package.json b/packages/zip/package.json index 4b323cd0d..79a5e673a 100644 --- a/packages/zip/package.json +++ b/packages/zip/package.json @@ -12,8 +12,9 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "eslint . --ext .js --cache", - "posttest": "yarn lint" + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "files": [ "index.js", diff --git a/yarn.lock b/yarn.lock index 68709643b..90bbbf22f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2259,6 +2259,196 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz#10b2944ca559386590683392022a897eefd011d3" integrity sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw== +"@oxfmt/binding-android-arm-eabi@0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.41.0.tgz#09438448ab9479730582cd00fb86e4c33795a364" + integrity sha512-REfrqeMKGkfMP+m/ScX4f5jJBSmVNYcpoDF8vP8f8eYPDuPGZmzp56NIUsYmx3h7f6NzC6cE3gqh8GDWrJHCKw== + +"@oxfmt/binding-android-arm64@0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.41.0.tgz#b01eef08ff2a6be90f5805fe28339ec0130a955c" + integrity sha512-s0b1dxNgb2KomspFV2LfogC2XtSJB42POXF4bMCLJyvQmAGos4ZtjGPfQreToQEaY0FQFjz3030ggI36rF1q5g== + +"@oxfmt/binding-darwin-arm64@0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.41.0.tgz#a2e24b7c52d40e3bb01939fa9c994d56923294ce" + integrity sha512-EGXGualADbv/ZmamE7/2DbsrYmjoPlAmHEpTL4vapLF4EfVD6fr8/uQDFnPJkUBjiSWFJZtFNsGeN1B6V3owmA== + +"@oxfmt/binding-darwin-x64@0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.41.0.tgz#c114c0a30195a65cfe0247e2ffd000416df4d4bc" + integrity sha512-WxySJEvdQQYMmyvISH3qDpTvoS0ebnIP63IMxLLWowJyPp/AAH0hdWtlo+iGNK5y3eVfa5jZguwNaQkDKWpGSw== + +"@oxfmt/binding-freebsd-x64@0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.41.0.tgz#a9057a31860ec79970a16b5a0a801a51fab99168" + integrity sha512-Y2kzMkv3U3oyuYaR4wTfGjOTYTXiFC/hXmG0yVASKkbh02BJkvD98Ij8bIevr45hNZ0DmZEgqiXF+9buD4yMYQ== + +"@oxfmt/binding-linux-arm-gnueabihf@0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.41.0.tgz#1a2be3cbbf2d9e90808858ba8fc38b24023ac44c" + integrity sha512-ptazDjdUyhket01IjPTT6ULS1KFuBfTUU97osTP96X5y/0oso+AgAaJzuH81oP0+XXyrWIHbRzozSAuQm4p48g== + +"@oxfmt/binding-linux-arm-musleabihf@0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.41.0.tgz#f0a12feca29bfaf7437d94c32919ff3ac60c213f" + integrity sha512-UkoL2OKxFD+56bPEBcdGn+4juTW4HRv/T6w1dIDLnvKKWr6DbarB/mtHXlADKlFiJubJz8pRkttOR7qjYR6lTA== + +"@oxfmt/binding-linux-arm64-gnu@0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.41.0.tgz#b304c7ede72119f7fe7ce7861007c0ee6eb60c08" + integrity sha512-gofu0PuumSOHYczD8p62CPY4UF6ee+rSLZJdUXkpwxg6pILiwSDBIouPskjF/5nF3A7QZTz2O9KFNkNxxFN9tA== + +"@oxfmt/binding-linux-arm64-musl@0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.41.0.tgz#295d6ce8d846ca1a287165bf4ff6e7b3cd51f823" + integrity sha512-VfVZxL0+6RU86T8F8vKiDBa+iHsr8PAjQmKGBzSCAX70b6x+UOMFl+2dNihmKmUwqkCazCPfYjt6SuAPOeQJ3g== + +"@oxfmt/binding-linux-ppc64-gnu@0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.41.0.tgz#6c702f37bd69bb88bf78264eef5df9dbcdcada49" + integrity sha512-bwzokz2eGvdfJbc0i+zXMJ4BBjQPqg13jyWpEEZDOrBCQ91r8KeY2Mi2kUeuMTZNFXju+jcAbAbpyJxRGla0eg== + +"@oxfmt/binding-linux-riscv64-gnu@0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.41.0.tgz#9dac4f7f4cf10d94ade5a109554f810a43525595" + integrity sha512-POLM//PCH9uqDeNDwWL3b3DkMmI3oI2cU6hwc2lnztD1o7dzrQs3R9nq555BZ6wI7t2lyhT9CS+CRaz5X0XqLA== + +"@oxfmt/binding-linux-riscv64-musl@0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.41.0.tgz#ddd573ad4fc8e5d017ab27177eaabea700d68754" + integrity sha512-NNK7PzhFqLUwx/G12Xtm6scGv7UITvyGdAR5Y+TlqsG+essnuRWR4jRNODWRjzLZod0T3SayRbnkSIWMBov33w== + +"@oxfmt/binding-linux-s390x-gnu@0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.41.0.tgz#3300ee81681382c1ec0a4013807c362c27ef4821" + integrity sha512-qVf/zDC5cN9eKe4qI/O/m445er1IRl6swsSl7jHkqmOSVfknwCe5JXitYjZca+V/cNJSU/xPlC5EFMabMMFDpw== + +"@oxfmt/binding-linux-x64-gnu@0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.41.0.tgz#be9068d48bb521eea83ea6bea2b0e08b39d7638b" + integrity sha512-ojxYWu7vUb6ysYqVCPHuAPVZHAI40gfZ0PDtZAMwVmh2f0V8ExpPIKoAKr7/8sNbAXJBBpZhs2coypIo2jJX4w== + +"@oxfmt/binding-linux-x64-musl@0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.41.0.tgz#e0232abd879874823213b71016042d001a740acb" + integrity sha512-O2exZLBxoCMIv2vlvcbkdedazJPTdG0VSup+0QUCfYQtx751zCZNboX2ZUOiQ/gDTdhtXvSiot0h6GEGkOyalA== + +"@oxfmt/binding-openharmony-arm64@0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.41.0.tgz#3e5893cde8cae02494cbb5493de9c46613e77058" + integrity sha512-N+31/VoL+z+NNBt8viy3I4NaIdPbiYeOnB884LKqvXldaE2dRztdPv3q5ipfZYv0RwFp7JfqS4I27K/DSHCakg== + +"@oxfmt/binding-win32-arm64-msvc@0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.41.0.tgz#31d8dfeeddc855dbecc9f45d11cd892635d2e42f" + integrity sha512-Z7NAtu/RN8kjCQ1y5oDD0nTAeRswh3GJ93qwcW51srmidP7XPBmZbLlwERu1W5veCevQJtPS9xmkpcDTYsGIwQ== + +"@oxfmt/binding-win32-ia32-msvc@0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.41.0.tgz#873fb408a3e8952fae67763b225e4ea77356e926" + integrity sha512-uNxxP3l4bJ6VyzIeRqCmBU2Q0SkCFgIhvx9/9dJ9V8t/v+jP1IBsuaLwCXGR8JPHtkj4tFp+RHtUmU2ZYAUpMA== + +"@oxfmt/binding-win32-x64-msvc@0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.41.0.tgz#0adf46bca6908c6b49c790e598f4972b6f4cd191" + integrity sha512-49ZSpbZ1noozyPapE8SUOSm3IN0Ze4b5nkO+4+7fq6oEYQQJFhE0saj5k/Gg4oewVPdjn0L3ZFeWk2Vehjcw7A== + +"@oxlint/binding-android-arm-eabi@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.56.0.tgz#c62ca8fb50d8134cfcd107d98ff5c43cadbebd81" + integrity sha512-IyfYPthZyiSKwAv/dLjeO18SaK8MxLI9Yss2JrRDyweQAkuL3LhEy7pwIwI7uA3KQc1Vdn20kdmj3q0oUIQL6A== + +"@oxlint/binding-android-arm64@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-android-arm64/-/binding-android-arm64-1.56.0.tgz#685525cd82ecd1106913da63454b3d263550432b" + integrity sha512-Ga5zYrzH6vc/VFxhn6MmyUnYEfy9vRpwTIks99mY3j6Nz30yYpIkWryI0QKPCgvGUtDSXVLEaMum5nA+WrNOSg== + +"@oxlint/binding-darwin-arm64@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.56.0.tgz#4794c4775840137d26401f86476dbc5b56d3f23e" + integrity sha512-ogmbdJysnw/D4bDcpf1sPLpFThZ48lYp4aKYm10Z/6Nh1SON6NtnNhTNOlhEY296tDFItsZUz+2tgcSYqh8Eyw== + +"@oxlint/binding-darwin-x64@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.56.0.tgz#5e1be617a6a23d1f5973d2d12c4e004a77cd7085" + integrity sha512-x8QE1h+RAtQ2g+3KPsP6Fk/tdz6zJQUv5c7fTrJxXV3GHOo+Ry5p/PsogU4U+iUZg0rj6hS+E4xi+mnwwlDCWQ== + +"@oxlint/binding-freebsd-x64@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.56.0.tgz#04502145f8e7f2ee84c8ea200ed0a59f3a298f0a" + integrity sha512-6G+WMZvwJpMvY7my+/SHEjb7BTk/PFbePqLpmVmUJRIsJMy/UlyYqjpuh0RCgYYkPLcnXm1rUM04kbTk8yS1Yg== + +"@oxlint/binding-linux-arm-gnueabihf@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.56.0.tgz#c97cc9ee8e725483bf9dee078ef008d637159178" + integrity sha512-YYHBsk/sl7fYwQOok+6W5lBPeUEvisznV/HZD2IfZmF3Bns6cPC3Z0vCtSEOaAWTjYWN3jVsdu55jMxKlsdlhg== + +"@oxlint/binding-linux-arm-musleabihf@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.56.0.tgz#ed2d5a200eab3825f4bb7cf1a1c33c3f63722397" + integrity sha512-+AZK8rOUr78y8WT6XkDb04IbMRqauNV+vgT6f8ZLOH8wnpQ9i7Nol0XLxAu+Cq7Sb+J9wC0j6Km5hG8rj47/yQ== + +"@oxlint/binding-linux-arm64-gnu@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.56.0.tgz#e195667892d7fd104582fcaaf7c687aeef77e7d8" + integrity sha512-urse2SnugwJRojUkGSSeH2LPMaje5Q50yQtvtL9HFckiyeqXzoFwOAZqD5TR29R2lq7UHidfFDM9EGcchcbb8A== + +"@oxlint/binding-linux-arm64-musl@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.56.0.tgz#2e0abc6e034d0d6669715075173fa3bae79ec08e" + integrity sha512-rkTZkBfJ4TYLjansjSzL6mgZOdN5IvUnSq3oNJSLwBcNvy3dlgQtpHPrRxrCEbbcp7oQ6If0tkNaqfOsphYZ9g== + +"@oxlint/binding-linux-ppc64-gnu@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.56.0.tgz#e681dcf14e1ce99c6a440a64c3a3ba9d1415ffb6" + integrity sha512-uqL1kMH3u69/e1CH2EJhP3CP28jw2ExLsku4o8RVAZ7fySo9zOyI2fy9pVlTAp4voBLVgzndXi3SgtdyCTa2aA== + +"@oxlint/binding-linux-riscv64-gnu@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.56.0.tgz#ac1f7ebd85abc913474f4428eb6bb568f0d2b8ea" + integrity sha512-j0CcMBOgV6KsRaBdsebIeiy7hCjEvq2KdEsiULf2LZqAq0v1M1lWjelhCV57LxsqaIGChXFuFJ0RiFrSRHPhSg== + +"@oxlint/binding-linux-riscv64-musl@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.56.0.tgz#92a2c407404b13647eaec274ce59eca389df24d1" + integrity sha512-7VDOiL8cDG3DQ/CY3yKjbV1c4YPvc4vH8qW09Vv+5ukq3l/Kcyr6XGCd5NvxUmxqDb2vjMpM+eW/4JrEEsUetA== + +"@oxlint/binding-linux-s390x-gnu@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.56.0.tgz#bf38537cff65fca07c4fc5ce665ea17099cb8601" + integrity sha512-JGRpX0M+ikD3WpwJ7vKcHKV6Kg0dT52BW2Eu2BupXotYeqGXBrbY+QPkAyKO6MNgKozyTNaRh3r7g+VWgyAQYQ== + +"@oxlint/binding-linux-x64-gnu@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.56.0.tgz#14f3978d59e132e8b32036eb088a5a450a37a904" + integrity sha512-dNaICPvtmuxFP/VbqdofrLqdS3bM/AKJN3LMJD52si44ea7Be1cBk6NpfIahaysG9Uo+L98QKddU9CD5L8UHnQ== + +"@oxlint/binding-linux-x64-musl@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.56.0.tgz#a601f23531cff6057dc4a9eb8c96cc2b6100f26d" + integrity sha512-pF1vOtM+GuXmbklM1hV8WMsn6tCNPvkUzklj/Ej98JhlanbmA2RB1BILgOpwSuCTRTIYx2MXssmEyQQ90QF5aA== + +"@oxlint/binding-openharmony-arm64@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.56.0.tgz#1be4f2186ab23695f274173881ef7f1c25c1165b" + integrity sha512-bp8NQ4RE6fDIFLa4bdBiOA+TAvkNkg+rslR+AvvjlLTYXLy9/uKAYLQudaQouWihLD/hgkrXIKKzXi5IXOewwg== + +"@oxlint/binding-win32-arm64-msvc@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.56.0.tgz#f0fe81f7ceba13b846a9470d8004ad5c45999642" + integrity sha512-PxT4OJDfMOQBzo3OlzFb9gkoSD+n8qSBxyVq2wQSZIHFQYGEqIRTo9M0ZStvZm5fdhMqaVYpOnJvH2hUMEDk/g== + +"@oxlint/binding-win32-ia32-msvc@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.56.0.tgz#ee097fa11e7bacdbb3f925f8aca14ed1fce8a51e" + integrity sha512-PTRy6sIEPqy2x8PTP1baBNReN/BNEFmde0L+mYeHmjXE1Vlcc9+I5nsqENsB2yAm5wLkzPoTNCMY/7AnabT4/A== + +"@oxlint/binding-win32-x64-msvc@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.56.0.tgz#68cc0cd4bc9da1ccff9164ef1779bbcde369cb4d" + integrity sha512-ZHa0clocjLmIDr+1LwoWtxRcoYniAvERotvwKUYKhH41NVfl0Y4LNbyQkwMZzwDvKklKGvGZ5+DAG58/Ik47tQ== + "@paralleldrive/cuid2@^2.2.2": version "2.3.1" resolved "https://registry.yarnpkg.com/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz#3d62ea9e7be867d3fa94b9897fab5b0ae187d784" @@ -6673,6 +6863,58 @@ outvariant@^1.4.0, outvariant@^1.4.3: resolved "https://registry.yarnpkg.com/outvariant/-/outvariant-1.4.3.tgz#221c1bfc093e8fec7075497e7799fdbf43d14873" integrity sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA== +oxfmt@0.41.0: + version "0.41.0" + resolved "https://registry.yarnpkg.com/oxfmt/-/oxfmt-0.41.0.tgz#7dbfb63d19704a85412f36678c3ace092ce88673" + integrity sha512-sKLdJZdQ3bw6x9qKiT7+eID4MNEXlDHf5ZacfIircrq6Qwjk0L6t2/JQlZZrVHTXJawK3KaMuBoJnEJPcqCEdg== + dependencies: + tinypool "2.1.0" + optionalDependencies: + "@oxfmt/binding-android-arm-eabi" "0.41.0" + "@oxfmt/binding-android-arm64" "0.41.0" + "@oxfmt/binding-darwin-arm64" "0.41.0" + "@oxfmt/binding-darwin-x64" "0.41.0" + "@oxfmt/binding-freebsd-x64" "0.41.0" + "@oxfmt/binding-linux-arm-gnueabihf" "0.41.0" + "@oxfmt/binding-linux-arm-musleabihf" "0.41.0" + "@oxfmt/binding-linux-arm64-gnu" "0.41.0" + "@oxfmt/binding-linux-arm64-musl" "0.41.0" + "@oxfmt/binding-linux-ppc64-gnu" "0.41.0" + "@oxfmt/binding-linux-riscv64-gnu" "0.41.0" + "@oxfmt/binding-linux-riscv64-musl" "0.41.0" + "@oxfmt/binding-linux-s390x-gnu" "0.41.0" + "@oxfmt/binding-linux-x64-gnu" "0.41.0" + "@oxfmt/binding-linux-x64-musl" "0.41.0" + "@oxfmt/binding-openharmony-arm64" "0.41.0" + "@oxfmt/binding-win32-arm64-msvc" "0.41.0" + "@oxfmt/binding-win32-ia32-msvc" "0.41.0" + "@oxfmt/binding-win32-x64-msvc" "0.41.0" + +oxlint@1.56.0: + version "1.56.0" + resolved "https://registry.yarnpkg.com/oxlint/-/oxlint-1.56.0.tgz#b538b0171c5560212ffd38b479aca9efa6b8dafc" + integrity sha512-Q+5Mj5PVaH/R6/fhMMFzw4dT+KPB+kQW4kaL8FOIq7tfhlnEVp6+3lcWqFruuTNlUo9srZUW3qH7Id4pskeR6g== + optionalDependencies: + "@oxlint/binding-android-arm-eabi" "1.56.0" + "@oxlint/binding-android-arm64" "1.56.0" + "@oxlint/binding-darwin-arm64" "1.56.0" + "@oxlint/binding-darwin-x64" "1.56.0" + "@oxlint/binding-freebsd-x64" "1.56.0" + "@oxlint/binding-linux-arm-gnueabihf" "1.56.0" + "@oxlint/binding-linux-arm-musleabihf" "1.56.0" + "@oxlint/binding-linux-arm64-gnu" "1.56.0" + "@oxlint/binding-linux-arm64-musl" "1.56.0" + "@oxlint/binding-linux-ppc64-gnu" "1.56.0" + "@oxlint/binding-linux-riscv64-gnu" "1.56.0" + "@oxlint/binding-linux-riscv64-musl" "1.56.0" + "@oxlint/binding-linux-s390x-gnu" "1.56.0" + "@oxlint/binding-linux-x64-gnu" "1.56.0" + "@oxlint/binding-linux-x64-musl" "1.56.0" + "@oxlint/binding-openharmony-arm64" "1.56.0" + "@oxlint/binding-win32-arm64-msvc" "1.56.0" + "@oxlint/binding-win32-ia32-msvc" "1.56.0" + "@oxlint/binding-win32-x64-msvc" "1.56.0" + p-cancelable@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-4.0.1.tgz#2d1edf1ab8616b72c73db41c4bc9ecdd10af640e" @@ -7779,6 +8021,11 @@ tinyglobby@^0.2.12, tinyglobby@^0.2.14, tinyglobby@^0.2.15: fdir "^6.5.0" picomatch "^4.0.3" +tinypool@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-2.1.0.tgz#303a671d6ef68d03c9512cdc9a47c86b8a85f20c" + integrity sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw== + tinypool@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.1.1.tgz#059f2d042bd37567fbc017d3d426bdd2a2612591" From a26fbd97692f245669cea2568046fccaa138f02c Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Tue, 24 Mar 2026 07:05:28 -0500 Subject: [PATCH 2/5] style: apply oxfmt close-to-default formatting sweep --- .oxfmtrc.json | 7 - packages/.eslintrc.js | 8 +- packages/api-framework/README.md | 6 +- packages/api-framework/index.js | 2 +- packages/api-framework/lib/Frame.js | 28 +- packages/api-framework/lib/api-framework.js | 16 +- packages/api-framework/lib/headers.js | 60 +- packages/api-framework/lib/http.js | 47 +- packages/api-framework/lib/pipeline.js | 73 +- .../api-framework/lib/serializers/handle.js | 20 +- .../api-framework/lib/serializers/index.js | 8 +- .../lib/serializers/input/all.js | 14 +- .../lib/serializers/input/index.js | 4 +- packages/api-framework/lib/utils/index.js | 4 +- packages/api-framework/lib/utils/options.js | 10 +- .../api-framework/lib/validators/handle.js | 12 +- .../api-framework/lib/validators/index.js | 6 +- .../api-framework/lib/validators/input/all.js | 159 ++-- .../lib/validators/input/index.js | 4 +- packages/api-framework/package.json | 18 +- packages/api-framework/test/.eslintrc.js | 6 +- .../api-framework/test/api-framework.test.js | 12 +- packages/api-framework/test/frame.test.js | 108 +-- packages/api-framework/test/headers.test.js | 277 ++++--- packages/api-framework/test/http.test.js | 106 ++- packages/api-framework/test/pipeline.test.js | 467 ++++++----- .../test/serializers/handle.test.js | 364 +++++---- .../test/serializers/input/all.test.js | 50 +- .../api-framework/test/util/options.test.js | 35 +- .../test/validators/handle.test.js | 53 +- .../test/validators/input/all.test.js | 436 +++++----- packages/bookshelf-collision/README.md | 12 +- packages/bookshelf-collision/index.js | 2 +- .../lib/bookshelf-collision.js | 38 +- packages/bookshelf-collision/package.json | 24 +- .../bookshelf-collision/test/.eslintrc.js | 6 +- .../test/bookshelf-collision.test.js | 155 ++-- packages/bookshelf-custom-query/README.md | 12 +- packages/bookshelf-custom-query/index.js | 2 +- .../lib/bookshelf-custom-query.js | 2 +- packages/bookshelf-custom-query/package.json | 18 +- .../bookshelf-custom-query/test/.eslintrc.js | 6 +- .../test/bookshelf-custom-query.test.js | 28 +- packages/bookshelf-eager-load/README.md | 12 +- packages/bookshelf-eager-load/index.js | 2 +- .../lib/bookshelf-eager-load.js | 31 +- packages/bookshelf-eager-load/package.json | 24 +- .../bookshelf-eager-load/test/.eslintrc.js | 6 +- .../test/bookshelf-eager-load.test.js | 154 ++-- packages/bookshelf-filter/README.md | 12 +- packages/bookshelf-filter/index.js | 2 +- .../bookshelf-filter/lib/bookshelf-filter.js | 24 +- packages/bookshelf-filter/package.json | 24 +- packages/bookshelf-filter/test/.eslintrc.js | 6 +- .../test/bookshelf-filter.test.js | 107 +-- packages/bookshelf-has-posts/README.md | 12 +- packages/bookshelf-has-posts/index.js | 2 +- .../lib/bookshelf-has-posts.js | 27 +- packages/bookshelf-has-posts/package.json | 24 +- .../bookshelf-has-posts/test/.eslintrc.js | 6 +- .../test/bookshelf-has-posts.test.js | 123 +-- packages/bookshelf-include-count/README.md | 12 +- packages/bookshelf-include-count/index.js | 2 +- .../lib/bookshelf-include-count.js | 32 +- packages/bookshelf-include-count/package.json | 24 +- .../bookshelf-include-count/test/.eslintrc.js | 6 +- .../test/bookshelf-include-count.test.js | 143 ++-- packages/bookshelf-order/README.md | 12 +- packages/bookshelf-order/index.js | 2 +- .../bookshelf-order/lib/bookshelf-order.js | 16 +- packages/bookshelf-order/package.json | 22 +- packages/bookshelf-order/test/.eslintrc.js | 6 +- .../test/bookshelf-order.test.js | 79 +- packages/bookshelf-pagination/README.md | 12 +- packages/bookshelf-pagination/index.js | 2 +- .../lib/bookshelf-pagination.js | 205 ++--- packages/bookshelf-pagination/package.json | 24 +- .../bookshelf-pagination/test/.eslintrc.js | 6 +- .../test/pagination.test.js | 308 +++---- packages/bookshelf-plugins/README.md | 12 +- packages/bookshelf-plugins/index.js | 2 +- .../lib/bookshelf-plugins.js | 20 +- packages/bookshelf-plugins/package.json | 24 +- packages/bookshelf-plugins/test/.eslintrc.js | 6 +- .../test/bookshelf-plugins.test.js | 32 +- packages/bookshelf-search/README.md | 12 +- packages/bookshelf-search/index.js | 2 +- .../bookshelf-search/lib/bookshelf-search.js | 2 +- packages/bookshelf-search/package.json | 18 +- packages/bookshelf-search/test/.eslintrc.js | 6 +- .../test/bookshelf-search.test.js | 30 +- .../bookshelf-transaction-events/README.md | 12 +- .../bookshelf-transaction-events/index.js | 2 +- .../lib/bookshelf-transaction-events.js | 4 +- .../bookshelf-transaction-events/package.json | 18 +- .../test/.eslintrc.js | 6 +- .../test/bookshelf-transaction-events.test.js | 44 +- packages/config/README.md | 12 +- packages/config/index.js | 2 +- packages/config/lib/config.js | 2 +- packages/config/lib/get-config.js | 25 +- packages/config/package.json | 18 +- packages/config/test/.eslintrc.js | 6 +- packages/config/test/config.test.js | 63 +- packages/database-info/README.md | 12 +- packages/database-info/index.js | 2 +- packages/database-info/lib/DatabaseInfo.js | 80 +- packages/database-info/package.json | 18 +- packages/database-info/test/.eslintrc.js | 6 +- .../database-info/test/DatabaseInfo.test.js | 156 ++-- packages/debug/README.md | 12 +- packages/debug/index.js | 2 +- packages/debug/lib/debug.js | 12 +- packages/debug/package.json | 18 +- packages/debug/test/.eslintrc.js | 6 +- packages/debug/test/debug.test.js | 87 +- packages/domain-events/README.md | 6 +- packages/domain-events/index.js | 2 +- packages/domain-events/lib/DomainEvents.js | 12 +- packages/domain-events/lib/index.d.ts | 2 +- packages/domain-events/package.json | 26 +- packages/domain-events/test/.eslintrc.js | 6 +- .../domain-events/test/DomainEvents.test.js | 39 +- packages/elasticsearch/README.md | 9 +- packages/elasticsearch/index.js | 4 +- packages/elasticsearch/lib/ElasticSearch.js | 18 +- .../elasticsearch/lib/ElasticSearchBunyan.js | 10 +- packages/elasticsearch/package.json | 24 +- packages/elasticsearch/test/.eslintrc.js | 6 +- .../elasticsearch/test/ElasticSearch.test.js | 88 +- packages/email-mock-receiver/README.md | 5 +- packages/email-mock-receiver/index.js | 2 +- .../lib/EmailMockReceiver.js | 29 +- packages/email-mock-receiver/package.json | 18 +- .../email-mock-receiver/test/.eslintrc.js | 10 +- .../test/EmailMockReceiver.test.js | 236 +++--- packages/errors/.eslintrc.js | 6 +- packages/errors/README.md | 8 +- packages/errors/package.json | 30 +- packages/errors/src/GhostError.ts | 20 +- packages/errors/src/errors.ts | 369 +++++---- packages/errors/src/index.ts | 12 +- packages/errors/src/utils.ts | 88 +- packages/errors/src/wrap-stack.ts | 4 +- packages/errors/test/.eslintrc.js | 6 +- packages/errors/test/errors.test.ts | 670 ++++++++++----- packages/errors/test/utils.test.ts | 52 +- packages/errors/test/wrap-stack.test.ts | 29 +- packages/errors/tsconfig.json | 4 +- packages/errors/vitest.config.ts | 21 +- packages/express-test/CLAUDE.md | 24 +- packages/express-test/README.md | 15 +- packages/express-test/example/app.js | 77 +- packages/express-test/example/data.json | 6 +- packages/express-test/example/user.json | 2 +- packages/express-test/index.js | 12 +- packages/express-test/lib/Agent.js | 39 +- packages/express-test/lib/ExpectRequest.js | 49 +- packages/express-test/lib/Request.js | 66 +- packages/express-test/lib/utils.js | 16 +- packages/express-test/package.json | 28 +- packages/express-test/test/.eslintrc.js | 10 +- packages/express-test/test/Agent.test.js | 243 +++--- .../express-test/test/ExpectRequest.test.js | 329 ++++---- packages/express-test/test/Request.test.js | 253 +++--- .../express-test/test/example-app.test.js | 520 ++++++------ packages/express-test/test/utils.test.js | 114 +-- packages/express-test/test/utils/index.js | 12 +- packages/express-test/test/utils/overrides.js | 10 +- packages/express-test/vitest.config.ts | 17 +- packages/http-cache-utils/README.md | 10 +- packages/http-cache-utils/index.js | 2 +- .../http-cache-utils/lib/http-cache-utils.js | 23 +- packages/http-cache-utils/package.json | 18 +- packages/http-cache-utils/test/.eslintrc.js | 6 +- .../test/http-cache-utils.test.js | 42 +- packages/http-stream/README.md | 12 +- packages/http-stream/index.js | 2 +- packages/http-stream/lib/HttpStream.js | 20 +- packages/http-stream/package.json | 24 +- packages/http-stream/test/.eslintrc.js | 6 +- packages/http-stream/test/HttpStream.test.js | 38 +- packages/jest-snapshot/.eslintrc.js | 10 +- packages/jest-snapshot/README.md | 12 +- packages/jest-snapshot/index.js | 2 +- packages/jest-snapshot/lib/SnapshotManager.js | 56 +- packages/jest-snapshot/lib/jest-snapshot.js | 17 +- packages/jest-snapshot/lib/utils.js | 9 +- packages/jest-snapshot/package.json | 24 +- packages/jest-snapshot/test/.eslintrc.js | 6 +- .../test/SnapshotManager.test.js | 220 ++--- .../jest-snapshot/test/jest-snapshot.test.js | 144 ++-- packages/jest-snapshot/test/utils.test.js | 15 +- packages/jest-snapshot/test/utils/index.js | 4 +- packages/job-manager/README.md | 86 +- packages/job-manager/index.js | 2 +- packages/job-manager/lib/JobManager.js | 174 ++-- packages/job-manager/lib/JobsRepository.js | 12 +- packages/job-manager/lib/assemble-bree-job.js | 14 +- .../job-manager/lib/is-cron-expression.js | 8 +- packages/job-manager/package.json | 32 +- packages/job-manager/test/.eslintrc.js | 6 +- .../test/examples/graceful-shutdown.js | 24 +- .../test/examples/scheduled-one-off.js | 24 +- .../test/is-cron-expression.test.js | 46 +- packages/job-manager/test/job-manager.test.js | 629 +++++++------- packages/job-manager/test/jobs/graceful.js | 16 +- .../test/jobs/inline-module-throws.js | 2 +- packages/job-manager/test/jobs/message.js | 16 +- packages/job-manager/test/jobs/timed-job.js | 6 +- packages/job-manager/vitest.config.ts | 17 +- packages/logging/README.md | 10 +- packages/logging/index.js | 2 +- packages/logging/lib/GhostLogger.js | 409 +++++----- packages/logging/lib/logging.js | 12 +- packages/logging/package.json | 26 +- packages/logging/test/.eslintrc.js | 6 +- packages/logging/test/logging.test.js | 770 +++++++++--------- packages/metrics/README.md | 12 +- packages/metrics/index.js | 2 +- packages/metrics/lib/GhostMetrics.js | 30 +- packages/metrics/lib/metrics.js | 8 +- packages/metrics/package.json | 26 +- packages/metrics/test/.eslintrc.js | 6 +- packages/metrics/test/metrics.test.js | 144 ++-- packages/mw-error-handler/README.md | 10 +- packages/mw-error-handler/index.js | 2 +- .../mw-error-handler/lib/mw-error-handler.js | 230 +++--- packages/mw-error-handler/package.json | 24 +- packages/mw-error-handler/test/.eslintrc.js | 6 +- .../test/mw-error-handler.test.js | 573 +++++++------ packages/mw-vhost/README.md | 127 +-- packages/mw-vhost/index.js | 2 +- packages/mw-vhost/lib/vhost.js | 39 +- packages/mw-vhost/package.json | 20 +- packages/mw-vhost/test/.eslintrc.js | 6 +- packages/mw-vhost/test/vhost.test.js | 194 ++--- packages/nodemailer/README.md | 18 +- packages/nodemailer/index.js | 2 +- packages/nodemailer/lib/nodemailer.js | 137 ++-- packages/nodemailer/package.json | 24 +- packages/nodemailer/test/.eslintrc.js | 6 +- packages/nodemailer/test/transporter.test.js | 126 +-- packages/pretty-cli/index.js | 2 +- packages/pretty-cli/lib/pretty-cli.js | 16 +- packages/pretty-cli/lib/styles.js | 20 +- packages/pretty-cli/lib/ui.js | 16 +- packages/pretty-cli/package.json | 24 +- packages/pretty-cli/test/.eslintrc.js | 6 +- packages/pretty-cli/test/pretty-cli.test.js | 127 +-- packages/pretty-stream/README.md | 11 +- packages/pretty-stream/index.js | 2 +- packages/pretty-stream/lib/PrettyStream.js | 248 +++--- packages/pretty-stream/package.json | 24 +- packages/pretty-stream/test/.eslintrc.js | 6 +- .../pretty-stream/test/PrettyStream.test.js | 526 ++++++------ packages/prometheus-metrics/.eslintrc.js | 8 +- packages/prometheus-metrics/README.md | 6 +- packages/prometheus-metrics/package.json | 24 +- .../prometheus-metrics/src/MetricsServer.ts | 30 +- .../src/PrometheusClient.ts | 177 ++-- packages/prometheus-metrics/src/index.ts | 4 +- .../prometheus-metrics/src/libraries.d.ts | 4 +- packages/prometheus-metrics/test/.eslintrc.js | 12 +- .../test/metrics-server.test.ts | 71 +- .../test/prometheus-client.test.ts | 707 +++++++++------- packages/prometheus-metrics/tsconfig.json | 210 +++-- packages/prometheus-metrics/vitest.config.ts | 21 +- packages/promise/README.md | 8 +- packages/promise/index.js | 8 +- packages/promise/lib/pool.js | 10 +- packages/promise/package.json | 18 +- packages/promise/test/.eslintrc.js | 6 +- packages/promise/test/pipeline.test.js | 68 +- packages/promise/test/pool.test.js | 63 +- packages/promise/test/sequence.test.js | 25 +- packages/request/README.md | 12 +- packages/request/index.js | 2 +- packages/request/lib/request.js | 39 +- packages/request/package.json | 28 +- packages/request/test/.eslintrc.js | 6 +- packages/request/test/index.test.js | 10 +- packages/request/test/request.test.js | 282 +++---- packages/root-utils/README.md | 12 +- packages/root-utils/index.js | 2 +- packages/root-utils/lib/root-utils.js | 14 +- packages/root-utils/package.json | 24 +- packages/root-utils/test/.eslintrc.js | 6 +- packages/root-utils/test/root-utils.test.ts | 68 +- packages/security/README.md | 8 +- packages/security/index.js | 14 +- packages/security/lib/identifier.js | 4 +- packages/security/lib/password.js | 4 +- packages/security/lib/secret.js | 6 +- packages/security/lib/string.js | 4 +- packages/security/lib/tokens.js | 46 +- packages/security/lib/url.js | 6 +- packages/security/package.json | 20 +- packages/security/test/.eslintrc.js | 6 +- packages/security/test/identifier.test.js | 8 +- packages/security/test/password.test.js | 20 +- packages/security/test/secret.test.js | 30 +- packages/security/test/string.test.js | 122 +-- packages/security/test/tokens.test.js | 138 ++-- packages/security/test/url.test.js | 16 +- packages/server/README.md | 12 +- packages/server/index.js | 2 +- packages/server/lib/server.js | 47 +- packages/server/package.json | 24 +- packages/server/test/.eslintrc.js | 6 +- packages/server/test/index.test.js | 10 +- packages/server/test/server.test.js | 238 +++--- packages/tpl/README.md | 16 +- packages/tpl/index.js | 2 +- packages/tpl/lib/tpl.js | 4 +- packages/tpl/package.json | 22 +- packages/tpl/test/.eslintrc.js | 6 +- packages/tpl/test/lodash.test.js | 10 +- packages/tpl/test/tpl.test.js | 84 +- packages/tsconfig.json | 28 +- packages/validator/README.md | 12 +- packages/validator/index.js | 4 +- packages/validator/lib/is-byte-length.js | 9 +- packages/validator/lib/is-email.js | 67 +- packages/validator/lib/is-fqdn.js | 21 +- packages/validator/lib/is-ip.js | 34 +- packages/validator/lib/util/assert-string.js | 10 +- packages/validator/lib/util/merge.js | 2 +- packages/validator/lib/validate.js | 42 +- packages/validator/lib/validator.js | 38 +- packages/validator/package.json | 18 +- packages/validator/test/.eslintrc.js | 6 +- packages/validator/test/internals.test.js | 222 ++--- packages/validator/test/validate.test.js | 12 +- packages/validator/test/validator.test.js | 81 +- packages/version/README.md | 10 +- packages/version/index.js | 2 +- packages/version/lib/version.js | 12 +- packages/version/package.json | 24 +- packages/version/test/.eslintrc.js | 6 +- packages/version/test/index.test.js | 10 +- packages/version/test/version.test.js | 62 +- packages/webhook-mock-receiver/README.md | 10 +- packages/webhook-mock-receiver/index.js | 2 +- .../lib/WebhookMockReceiver.js | 28 +- packages/webhook-mock-receiver/package.json | 22 +- .../webhook-mock-receiver/test/.eslintrc.js | 10 +- .../test/WebhookMockReceiver.test.js | 137 ++-- packages/zip/README.md | 7 +- packages/zip/index.js | 4 +- packages/zip/lib/compress.js | 18 +- packages/zip/lib/extract.js | 14 +- packages/zip/package.json | 26 +- packages/zip/test/.eslintrc.js | 12 +- packages/zip/test/zip.test.js | 97 ++- 355 files changed, 9911 insertions(+), 8741 deletions(-) diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 76ea23648..718ff675e 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -1,11 +1,4 @@ { - "tabWidth": 4, - "singleQuote": true, - "semi": true, - "trailingComma": "none", - "quoteProps": "as-needed", - "bracketSpacing": false, - "arrowParens": "avoid", "ignorePatterns": [ "**/node_modules/**", "**/build/**", diff --git a/packages/.eslintrc.js b/packages/.eslintrc.js index 7b4b5703d..a0af5f89d 100644 --- a/packages/.eslintrc.js +++ b/packages/.eslintrc.js @@ -1,7 +1,5 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/node' - ], - ignorePatterns: ['**/build/**'] + plugins: ["ghost"], + extends: ["plugin:ghost/node"], + ignorePatterns: ["**/build/**"], }; diff --git a/packages/api-framework/README.md b/packages/api-framework/README.md index 61ebbe7da..c8088742e 100644 --- a/packages/api-framework/README.md +++ b/packages/api-framework/README.md @@ -20,7 +20,6 @@ Each request goes through the following stages: The framework we are building pipes a request through these stages in respect of the API controller configuration. - ### Frame Is a class, which holds all the information for request processing. We pass this instance by reference. @@ -78,7 +77,6 @@ edit: { #### Examples - ``` edit: { headers: { @@ -142,13 +140,11 @@ edit: { This is a monorepo package. Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests - diff --git a/packages/api-framework/index.js b/packages/api-framework/index.js index 2fd7fa32f..f93fa5ccc 100644 --- a/packages/api-framework/index.js +++ b/packages/api-framework/index.js @@ -24,4 +24,4 @@ * @typedef {Record & Record<'docName', string>} Controller */ -module.exports = require('./lib/api-framework'); +module.exports = require("./lib/api-framework"); diff --git a/packages/api-framework/lib/Frame.js b/packages/api-framework/lib/Frame.js index 70d265bb8..b40cec9a2 100644 --- a/packages/api-framework/lib/Frame.js +++ b/packages/api-framework/lib/Frame.js @@ -1,5 +1,5 @@ -const debug = require('@tryghost/debug')('frame'); -const _ = require('lodash'); +const debug = require("@tryghost/debug")("frame"); +const _ = require("lodash"); /** * @description The "frame" holds all information of a request. @@ -45,22 +45,22 @@ class Frame { * Based on the API ctrl implemented, this fn will pick allowed properties to either options or data. */ configure(apiConfig) { - debug('configure'); + debug("configure"); if (apiConfig.options) { - if (typeof apiConfig.options === 'function') { + if (typeof apiConfig.options === "function") { apiConfig.options = apiConfig.options(this); } - if (Object.prototype.hasOwnProperty.call(this.original, 'query')) { + if (Object.prototype.hasOwnProperty.call(this.original, "query")) { Object.assign(this.options, _.pick(this.original.query, apiConfig.options)); } - if (Object.prototype.hasOwnProperty.call(this.original, 'params')) { + if (Object.prototype.hasOwnProperty.call(this.original, "params")) { Object.assign(this.options, _.pick(this.original.params, apiConfig.options)); } - if (Object.prototype.hasOwnProperty.call(this.original, 'options')) { + if (Object.prototype.hasOwnProperty.call(this.original, "options")) { Object.assign(this.options, _.pick(this.original.options, apiConfig.options)); } } @@ -71,19 +71,19 @@ class Frame { this.data = _.cloneDeep(this.original.body); } else { if (apiConfig.data) { - if (typeof apiConfig.data === 'function') { + if (typeof apiConfig.data === "function") { apiConfig.data = apiConfig.data(this); } - if (Object.prototype.hasOwnProperty.call(this.original, 'query')) { + if (Object.prototype.hasOwnProperty.call(this.original, "query")) { Object.assign(this.data, _.pick(this.original.query, apiConfig.data)); } - if (Object.prototype.hasOwnProperty.call(this.original, 'params')) { + if (Object.prototype.hasOwnProperty.call(this.original, "params")) { Object.assign(this.data, _.pick(this.original.params, apiConfig.data)); } - if (Object.prototype.hasOwnProperty.call(this.original, 'options')) { + if (Object.prototype.hasOwnProperty.call(this.original, "options")) { Object.assign(this.data, _.pick(this.original.options, apiConfig.data)); } } @@ -93,9 +93,9 @@ class Frame { this.file = this.original.file; this.files = this.original.files; - debug('original', this.original); - debug('options', this.options); - debug('data', this.data); + debug("original", this.original); + debug("options", this.options); + debug("data", this.data); } setHeader(header, value) { diff --git a/packages/api-framework/lib/api-framework.js b/packages/api-framework/lib/api-framework.js index 28fa94216..11705ea47 100644 --- a/packages/api-framework/lib/api-framework.js +++ b/packages/api-framework/lib/api-framework.js @@ -1,29 +1,29 @@ module.exports = { get headers() { - return require('./headers'); + return require("./headers"); }, get http() { - return require('./http'); + return require("./http"); }, get Frame() { - return require('./Frame'); + return require("./Frame"); }, get pipeline() { - return require('./pipeline'); + return require("./pipeline"); }, get validators() { - return require('./validators'); + return require("./validators"); }, get serializers() { - return require('./serializers'); + return require("./serializers"); }, get utils() { - return require('./utils'); - } + return require("./utils"); + }, }; diff --git a/packages/api-framework/lib/headers.js b/packages/api-framework/lib/headers.js index 165baeb37..9a0c1a63a 100644 --- a/packages/api-framework/lib/headers.js +++ b/packages/api-framework/lib/headers.js @@ -1,12 +1,12 @@ -const url = require('url'); -const debug = require('@tryghost/debug')('headers'); -const INVALIDATE_ALL = '/*'; +const url = require("url"); +const debug = require("@tryghost/debug")("headers"); +const INVALIDATE_ALL = "/*"; const cacheInvalidate = (result, options = {}) => { let value = options.value; return { - 'X-Cache-Invalidate': value || INVALIDATE_ALL + "X-Cache-Invalidate": value || INVALIDATE_ALL, }; }; @@ -21,13 +21,13 @@ const disposition = { csv(result, options = {}) { let value = options.value; - if (typeof options.value === 'function') { + if (typeof options.value === "function") { value = options.value(); } return { - 'Content-Disposition': `Attachment; filename="${value}"`, - 'Content-Type': 'text/csv' + "Content-Disposition": `Attachment; filename="${value}"`, + "Content-Type": "text/csv", }; }, @@ -40,9 +40,9 @@ const disposition = { */ json(result, options = {}) { return { - 'Content-Disposition': `Attachment; filename="${options.value}"`, - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(JSON.stringify(result)) + "Content-Disposition": `Attachment; filename="${options.value}"`, + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(JSON.stringify(result)), }; }, @@ -55,9 +55,9 @@ const disposition = { */ yaml(result, options = {}) { return { - 'Content-Disposition': `Attachment; filename="${options.value}"`, - 'Content-Type': 'application/yaml', - 'Content-Length': Buffer.byteLength(JSON.stringify(result)) + "Content-Disposition": `Attachment; filename="${options.value}"`, + "Content-Type": "application/yaml", + "Content-Length": Buffer.byteLength(JSON.stringify(result)), }; }, @@ -79,7 +79,7 @@ const disposition = { .then(() => { let value = options.value; - if (typeof options.value === 'function') { + if (typeof options.value === "function") { value = options.value(); } @@ -87,10 +87,10 @@ const disposition = { }) .then((filename) => { return { - 'Content-Disposition': `Attachment; filename="${filename}"` + "Content-Disposition": `Attachment; filename="${filename}"`, }; }); - } + }, }; module.exports = { @@ -106,7 +106,10 @@ module.exports = { let headers = {}; if (apiConfigHeaders.disposition) { - const dispositionHeader = await disposition[apiConfigHeaders.disposition.type](result, apiConfigHeaders.disposition); + const dispositionHeader = await disposition[apiConfigHeaders.disposition.type]( + result, + apiConfigHeaders.disposition, + ); if (dispositionHeader) { Object.assign(headers, dispositionHeader); @@ -114,7 +117,10 @@ module.exports = { } if (apiConfigHeaders.cacheInvalidate) { - const cacheInvalidationHeader = cacheInvalidate(result, apiConfigHeaders.cacheInvalidate); + const cacheInvalidationHeader = cacheInvalidate( + result, + apiConfigHeaders.cacheInvalidate, + ); if (cacheInvalidationHeader) { Object.assign(headers, cacheInvalidationHeader); @@ -123,15 +129,19 @@ module.exports = { const locationHeaderDisabled = apiConfigHeaders?.location === false; const hasLocationResolver = apiConfigHeaders?.location?.resolve; - const hasFrameData = (frame?.method === 'add' || hasLocationResolver) && result[frame.docName]?.[0]?.id; + const hasFrameData = + (frame?.method === "add" || hasLocationResolver) && result[frame.docName]?.[0]?.id; if (!locationHeaderDisabled && hasFrameData) { - const protocol = (frame.original.url.secure === false) ? 'http://' : 'https://'; + const protocol = frame.original.url.secure === false ? "http://" : "https://"; const resourceId = result[frame.docName][0].id; - let locationURL = url.resolve(`${protocol}${frame.original.url.host}`,frame.original.url.pathname); - if (!locationURL.endsWith('/')) { - locationURL += '/'; + let locationURL = url.resolve( + `${protocol}${frame.original.url.host}`, + frame.original.url.pathname, + ); + if (!locationURL.endsWith("/")) { + locationURL += "/"; } locationURL += `${resourceId}/`; @@ -141,7 +151,7 @@ module.exports = { } const locationHeader = { - Location: locationURL + Location: locationURL, }; Object.assign(headers, locationHeader); @@ -153,5 +163,5 @@ module.exports = { debug(headers); return headers; - } + }, }; diff --git a/packages/api-framework/lib/http.js b/packages/api-framework/lib/http.js index cf35d7055..3c80ea5ba 100644 --- a/packages/api-framework/lib/http.js +++ b/packages/api-framework/lib/http.js @@ -1,8 +1,8 @@ -const url = require('url'); -const debug = require('@tryghost/debug')('http'); +const url = require("url"); +const debug = require("@tryghost/debug")("http"); -const Frame = require('./Frame'); -const headers = require('./headers'); +const Frame = require("./Frame"); +const headers = require("./headers"); /** * @description HTTP wrapper. @@ -28,11 +28,11 @@ const http = (apiImpl) => { if (req.api_key) { apiKey = { - id: req.api_key.get('id'), - type: req.api_key.get('type') + id: req.api_key.get("id"), + type: req.api_key.get("type"), }; integration = { - id: req.api_key.get('integration_id') + id: req.api_key.get("integration_id"), }; } @@ -49,21 +49,21 @@ const http = (apiImpl) => { user: req.user, session: req.session, url: { - host: req.vhost ? req.vhost.host : req.get('host'), + host: req.vhost ? req.vhost.host : req.get("host"), pathname: url.parse(req.originalUrl || req.url).pathname, - secure: req.secure + secure: req.secure, }, context: { api_key: apiKey, user: user, integration: integration, - member: (req.member || null) - } + member: req.member || null, + }, }); frame.configure({ options: apiImpl.options, - data: apiImpl.data + data: apiImpl.data, }); try { @@ -72,13 +72,13 @@ const http = (apiImpl) => { debug(`External API request to ${frame.docName}.${frame.method}`); // CASE: api ctrl wants to handle the express response (e.g. streams) - if (typeof result === 'function') { - debug('ctrl function call'); + if (typeof result === "function") { + debug("ctrl function call"); return result(req, res, next); } let statusCode = 200; - if (typeof apiImpl.statusCode === 'function') { + if (typeof apiImpl.statusCode === "function") { statusCode = apiImpl.statusCode(result); } else if (apiImpl.statusCode) { statusCode = apiImpl.statusCode; @@ -87,26 +87,27 @@ const http = (apiImpl) => { res.status(statusCode); // CASE: generate headers based on the api ctrl configuration - const apiHeaders = await headers.get(result, apiImpl.headers, frame) || {}; + const apiHeaders = (await headers.get(result, apiImpl.headers, frame)) || {}; res.set(apiHeaders); const send = (format) => { - if (format === 'plain') { - debug('plain text response'); + if (format === "plain") { + debug("plain text response"); return res.send(result); } - debug('json response'); + debug("json response"); res.json(result || {}); }; let responseFormat; - if (apiImpl.response){ - if (typeof apiImpl.response.format === 'function') { + if (apiImpl.response) { + if (typeof apiImpl.response.format === "function") { const apiResponseFormat = apiImpl.response.format(); - if (apiResponseFormat.then) { // is promise + if (apiResponseFormat.then) { + // is promise return apiResponseFormat.then((formatName) => { send(formatName); }); @@ -122,7 +123,7 @@ const http = (apiImpl) => { } catch (err) { req.frameOptions = { docName: frame.docName, - method: frame.method + method: frame.method, }; next(err); diff --git a/packages/api-framework/lib/pipeline.js b/packages/api-framework/lib/pipeline.js index b8787cc53..569e820d3 100644 --- a/packages/api-framework/lib/pipeline.js +++ b/packages/api-framework/lib/pipeline.js @@ -1,11 +1,11 @@ -const debug = require('@tryghost/debug')('pipeline'); -const _ = require('lodash'); -const errors = require('@tryghost/errors'); -const {sequence} = require('@tryghost/promise'); +const debug = require("@tryghost/debug")("pipeline"); +const _ = require("lodash"); +const errors = require("@tryghost/errors"); +const { sequence } = require("@tryghost/promise"); -const Frame = require('./Frame'); -const serializers = require('./serializers'); -const validators = require('./validators'); +const Frame = require("./Frame"); +const serializers = require("./serializers"); +const validators = require("./validators"); const STAGES = { validation: { @@ -24,12 +24,12 @@ const STAGES = { * @return {Promise} */ input(apiUtils, apiConfig, apiImpl, frame) { - debug('stages: validation'); + debug("stages: validation"); const tasks = []; // CASE: do validation completely yourself - if (typeof apiImpl.validation === 'function') { - debug('validation function call'); + if (typeof apiImpl.validation === "function") { + debug("validation function call"); return apiImpl.validation(frame); } @@ -37,12 +37,12 @@ const STAGES = { return validators.handle.input( Object.assign({}, apiConfig, apiImpl.validation), apiUtils.validators.input, - frame + frame, ); }); return sequence(tasks); - } + }, }, serialisation: { @@ -61,11 +61,11 @@ const STAGES = { * @return {Promise} */ input(apiUtils, apiConfig, apiImpl, frame) { - debug('stages: input serialisation'); + debug("stages: input serialisation"); return serializers.handle.input( - Object.assign({data: apiImpl.data}, apiConfig), + Object.assign({ data: apiImpl.data }, apiConfig), apiUtils.serializers.input, - frame + frame, ); }, @@ -84,9 +84,14 @@ const STAGES = { * @return {Promise} */ output(response, apiUtils, apiConfig, apiImpl, frame) { - debug('stages: output serialisation'); - return serializers.handle.output(response, apiConfig, apiUtils.serializers.output, frame); - } + debug("stages: output serialisation"); + return serializers.handle.output( + response, + apiConfig, + apiUtils.serializers.output, + frame, + ); + }, }, /** @@ -103,27 +108,27 @@ const STAGES = { * @return {Promise} */ permissions(apiUtils, apiConfig, apiImpl, frame) { - debug('stages: permissions'); + debug("stages: permissions"); const tasks = []; // CASE: it's required to put the permission key to avoid security holes - if (!Object.prototype.hasOwnProperty.call(apiImpl, 'permissions')) { + if (!Object.prototype.hasOwnProperty.call(apiImpl, "permissions")) { return Promise.reject(new errors.IncorrectUsageError()); } // CASE: handle permissions completely yourself - if (typeof apiImpl.permissions === 'function') { - debug('permissions function call'); + if (typeof apiImpl.permissions === "function") { + debug("permissions function call"); return apiImpl.permissions(frame); } // CASE: skip stage completely if (apiImpl.permissions === false) { - debug('disabled permissions'); + debug("disabled permissions"); return Promise.resolve(); } - if (typeof apiImpl.permissions === 'object' && apiImpl.permissions.before) { + if (typeof apiImpl.permissions === "object" && apiImpl.permissions.before) { tasks.push(function beforePermissions() { return apiImpl.permissions.before(frame); }); @@ -132,7 +137,7 @@ const STAGES = { tasks.push(function doPermissions() { return apiUtils.permissions.handle( Object.assign({}, apiConfig, apiImpl.permissions), - frame + frame, ); }); @@ -149,14 +154,14 @@ const STAGES = { * @return {Promise} */ query(apiUtils, apiConfig, apiImpl, frame) { - debug('stages: query'); + debug("stages: query"); if (!apiImpl.query) { return Promise.reject(new errors.IncorrectUsageError()); } return apiImpl.query(frame); - } + }, }; const controllerMap = new Map(); @@ -185,7 +190,7 @@ const pipeline = (apiController, apiUtils, apiType) => { return controllerMap.get(apiController); } - const keys = Object.keys(apiController).filter(key => key !== 'docName'); + const keys = Object.keys(apiController).filter((key) => key !== "docName"); const docName = apiController.docName; // CASE: api controllers are objects with configuration. @@ -196,7 +201,7 @@ const pipeline = (apiController, apiUtils, apiType) => { Object.freeze(apiImpl.headers); obj[method] = async function ImplWrapper() { - const apiConfig = {docName, method}; + const apiConfig = { docName, method }; let options; let data; let frame; @@ -215,21 +220,21 @@ const pipeline = (apiController, apiUtils, apiType) => { debug(`Internal API request for ${docName}.${method}`); frame = new Frame({ body: data, - options: _.omit(options, 'context'), - context: options.context || {} + options: _.omit(options, "context"), + context: options.context || {}, }); frame.configure({ options: apiImpl.options, - data: apiImpl.data + data: apiImpl.data, }); } else { frame = options; } // CASE: api controller *can* be a single function, but it's not recommended to disable the framework. - if (typeof apiImpl === 'function') { - debug('ctrl function call'); + if (typeof apiImpl === "function") { + debug("ctrl function call"); return apiImpl(frame); } diff --git a/packages/api-framework/lib/serializers/handle.js b/packages/api-framework/lib/serializers/handle.js index 6356ebdbf..1be4e85fc 100644 --- a/packages/api-framework/lib/serializers/handle.js +++ b/packages/api-framework/lib/serializers/handle.js @@ -1,6 +1,6 @@ -const debug = require('@tryghost/debug')('serializers:handle'); -const {sequence} = require('@tryghost/promise'); -const errors = require('@tryghost/errors'); +const debug = require("@tryghost/debug")("serializers:handle"); +const { sequence } = require("@tryghost/promise"); +const errors = require("@tryghost/errors"); /** * @description Shared input serialization handler. @@ -15,10 +15,10 @@ const errors = require('@tryghost/errors'); * @param {import('@tryghost/api-framework').Frame} frame */ module.exports.input = (apiConfig, apiSerializers, frame) => { - debug('input'); + debug("input"); const tasks = []; - const sharedSerializers = require('./input'); + const sharedSerializers = require("./input"); if (!apiConfig) { return Promise.reject(new errors.IncorrectUsageError()); @@ -93,7 +93,7 @@ const getBestMatchSerializer = function (apiSerializers, docName, method) { * @param {import('@tryghost/api-framework').Frame} frame */ module.exports.output = (response = {}, apiConfig, apiSerializers, frame) => { - debug('output'); + debug("output"); const tasks = []; @@ -113,8 +113,12 @@ module.exports.output = (response = {}, apiConfig, apiSerializers, frame) => { }); } - const customSerializer = getBestMatchSerializer(apiSerializers, apiConfig.docName, apiConfig.method); - const defaultSerializer = getBestMatchSerializer(apiSerializers, 'default', apiConfig.method); + const customSerializer = getBestMatchSerializer( + apiSerializers, + apiConfig.docName, + apiConfig.method, + ); + const defaultSerializer = getBestMatchSerializer(apiSerializers, "default", apiConfig.method); if (customSerializer) { // CASE: custom serializer exists diff --git a/packages/api-framework/lib/serializers/index.js b/packages/api-framework/lib/serializers/index.js index ba10b3cbe..8b0452267 100644 --- a/packages/api-framework/lib/serializers/index.js +++ b/packages/api-framework/lib/serializers/index.js @@ -1,13 +1,13 @@ module.exports = { get handle() { - return require('./handle'); + return require("./handle"); }, get input() { - return require('./input'); + return require("./input"); }, get output() { - return require('./output'); - } + return require("./output"); + }, }; diff --git a/packages/api-framework/lib/serializers/input/all.js b/packages/api-framework/lib/serializers/input/all.js index 4a2a0ddad..7b37cc591 100644 --- a/packages/api-framework/lib/serializers/input/all.js +++ b/packages/api-framework/lib/serializers/input/all.js @@ -1,8 +1,8 @@ -const debug = require('@tryghost/debug')('serializers:input:all'); -const _ = require('lodash'); -const utils = require('../../utils'); +const debug = require("@tryghost/debug")("serializers:input:all"); +const _ = require("lodash"); +const utils = require("../../utils"); -const INTERNAL_OPTIONS = ['transacting', 'forUpdate']; +const INTERNAL_OPTIONS = ["transacting", "forUpdate"]; /** * @description Shared serializer for all requests. @@ -13,7 +13,7 @@ const INTERNAL_OPTIONS = ['transacting', 'forUpdate']; */ module.exports = { all(apiConfig, frame) { - debug('serialize all'); + debug("serialize all"); if (frame.options.include) { frame.options.withRelated = utils.options.trimAndLowerCase(frame.options.include); @@ -34,8 +34,8 @@ module.exports = { } if (!frame.options.context.internal) { - debug('omit internal options'); + debug("omit internal options"); frame.options = _.omit(frame.options, INTERNAL_OPTIONS); } - } + }, }; diff --git a/packages/api-framework/lib/serializers/input/index.js b/packages/api-framework/lib/serializers/input/index.js index 7b25869f4..de014d607 100644 --- a/packages/api-framework/lib/serializers/input/index.js +++ b/packages/api-framework/lib/serializers/input/index.js @@ -1,5 +1,5 @@ module.exports = { get all() { - return require('./all'); - } + return require("./all"); + }, }; diff --git a/packages/api-framework/lib/utils/index.js b/packages/api-framework/lib/utils/index.js index ce0fc5c43..1368ae1c9 100644 --- a/packages/api-framework/lib/utils/index.js +++ b/packages/api-framework/lib/utils/index.js @@ -1,5 +1,5 @@ module.exports = { get options() { - return require('./options'); - } + return require("./options"); + }, }; diff --git a/packages/api-framework/lib/utils/options.js b/packages/api-framework/lib/utils/options.js index 90b5d6eaa..61d22ffd1 100644 --- a/packages/api-framework/lib/utils/options.js +++ b/packages/api-framework/lib/utils/options.js @@ -1,5 +1,5 @@ -const _ = require('lodash'); -const {IncorrectUsageError} = require('@tryghost/errors'); +const _ = require("lodash"); +const { IncorrectUsageError } = require("@tryghost/errors"); /** * @description Helper function to prepare params for internal usages. @@ -10,17 +10,17 @@ const {IncorrectUsageError} = require('@tryghost/errors'); * @return {Array} */ const trimAndLowerCase = (params) => { - params = params || ''; + params = params || ""; if (_.isString(params)) { - params = params.split(','); + params = params.split(","); } // If we don't have an array at this point, something is wrong, so we should throw an // error to avoid trying to .map over something else if (!_.isArray(params)) { throw new IncorrectUsageError({ - message: 'Params must be a string or array' + message: "Params must be a string or array", }); } diff --git a/packages/api-framework/lib/validators/handle.js b/packages/api-framework/lib/validators/handle.js index 43f62f77d..8a163653b 100644 --- a/packages/api-framework/lib/validators/handle.js +++ b/packages/api-framework/lib/validators/handle.js @@ -1,6 +1,6 @@ -const debug = require('@tryghost/debug')('validators:handle'); -const errors = require('@tryghost/errors'); -const {sequence} = require('@tryghost/promise'); +const debug = require("@tryghost/debug")("validators:handle"); +const errors = require("@tryghost/errors"); +const { sequence } = require("@tryghost/promise"); /** * @description Shared input validation handler. @@ -15,10 +15,10 @@ const {sequence} = require('@tryghost/promise'); * @param {import('@tryghost/api-framework').Frame} frame */ module.exports.input = (apiConfig, apiValidators, frame) => { - debug('input begin'); + debug("input begin"); const tasks = []; - const sharedValidators = require('./input'); + const sharedValidators = require("./input"); if (!apiValidators) { return Promise.reject(new errors.IncorrectUsageError()); @@ -62,6 +62,6 @@ module.exports.input = (apiConfig, apiValidators, frame) => { } } - debug('input ready'); + debug("input ready"); return sequence(tasks); }; diff --git a/packages/api-framework/lib/validators/index.js b/packages/api-framework/lib/validators/index.js index 7830fcfee..65af64fd7 100644 --- a/packages/api-framework/lib/validators/index.js +++ b/packages/api-framework/lib/validators/index.js @@ -1,9 +1,9 @@ module.exports = { get handle() { - return require('./handle'); + return require("./handle"); }, get input() { - return require('./input'); - } + return require("./input"); + }, }; diff --git a/packages/api-framework/lib/validators/input/all.js b/packages/api-framework/lib/validators/input/all.js index 661600db5..3f944223a 100644 --- a/packages/api-framework/lib/validators/input/all.js +++ b/packages/api-framework/lib/validators/input/all.js @@ -1,33 +1,33 @@ -const debug = require('@tryghost/debug')('validators:input:all'); -const _ = require('lodash'); -const tpl = require('@tryghost/tpl'); -const {BadRequestError, ValidationError} = require('@tryghost/errors'); -const validator = require('@tryghost/validator'); +const debug = require("@tryghost/debug")("validators:input:all"); +const _ = require("lodash"); +const tpl = require("@tryghost/tpl"); +const { BadRequestError, ValidationError } = require("@tryghost/errors"); +const validator = require("@tryghost/validator"); const messages = { - validationFailed: 'Validation ({validationName}) failed for {key}', - noRootKeyProvided: 'No root key (\'{docName}\') provided.', - invalidIdProvided: 'Invalid id provided.' + validationFailed: "Validation ({validationName}) failed for {key}", + noRootKeyProvided: "No root key ('{docName}') provided.", + invalidIdProvided: "Invalid id provided.", }; const GLOBAL_VALIDATORS = { - id: {matches: /^[a-f\d]{24}$|^1$|me/i}, - page: {matches: /^\d+$/}, - limit: {matches: /^\d+|all$/}, - from: {isDate: true}, - to: {isDate: true}, - columns: {matches: /^[\w, ]+$/}, - order: {matches: /^[a-z0-9_,. ]+$/i}, - uuid: {isUUID: true}, - slug: {isSlug: true}, + id: { matches: /^[a-f\d]{24}$|^1$|me/i }, + page: { matches: /^\d+$/ }, + limit: { matches: /^\d+|all$/ }, + from: { isDate: true }, + to: { isDate: true }, + columns: { matches: /^[\w, ]+$/ }, + order: { matches: /^[a-z0-9_,. ]+$/i }, + uuid: { isUUID: true }, + slug: { isSlug: true }, name: {}, - email: {isEmail: true}, + email: { isEmail: true }, filter: false, context: false, forUpdate: false, transacting: false, include: false, - formats: false + formats: false, }; const validate = (config, attrs) => { @@ -35,12 +35,14 @@ const validate = (config, attrs) => { _.each(config, (value, key) => { if (value.required && !attrs[key]) { - errors.push(new ValidationError({ - message: tpl(messages.validationFailed, { - validationName: 'FieldIsRequired', - key: key - }) - })); + errors.push( + new ValidationError({ + message: tpl(messages.validationFailed, { + validationName: "FieldIsRequired", + key: key, + }), + }), + ); } }); @@ -48,7 +50,7 @@ const validate = (config, attrs) => { debug(key, value); if (GLOBAL_VALIDATORS[key]) { - debug('global validation'); + debug("global validation"); errors = errors.concat(validator.validate(value, key, GLOBAL_VALIDATORS[key])); } @@ -56,31 +58,35 @@ const validate = (config, attrs) => { const allowedValues = Array.isArray(config[key]) ? config[key] : config[key].values; if (allowedValues) { - debug('ctrl validation'); + debug("ctrl validation"); // CASE: we allow e.g. `formats=` if (!value || !value.length) { return; } - const valuesAsArray = Array.isArray(value) ? value : value.trim().toLowerCase().split(','); + const valuesAsArray = Array.isArray(value) + ? value + : value.trim().toLowerCase().split(","); const unallowedValues = _.filter(valuesAsArray, (valueToFilter) => { return !allowedValues.includes(valueToFilter); }); if (unallowedValues.length) { // CASE: we do not error for invalid includes, just silently remove - if (key === 'include') { - attrs.include = valuesAsArray.filter(x => allowedValues.includes(x)); + if (key === "include") { + attrs.include = valuesAsArray.filter((x) => allowedValues.includes(x)); return; } - errors.push(new ValidationError({ - message: tpl(messages.validationFailed, { - validationName: 'AllowedValues', - key: key - }) - })); + errors.push( + new ValidationError({ + message: tpl(messages.validationFailed, { + validationName: "AllowedValues", + key: key, + }), + }), + ); } } } @@ -95,7 +101,7 @@ module.exports = { * @param {import('@tryghost/api-framework').Frame} frame */ all(apiConfig, frame) { - debug('validate all'); + debug("validate all"); let validationErrors = validate(apiConfig.options, frame.options); @@ -111,7 +117,7 @@ module.exports = { * @param {import('@tryghost/api-framework').Frame} frame */ browse(apiConfig, frame) { - debug('validate browse'); + debug("validate browse"); let validationErrors = []; @@ -125,7 +131,7 @@ module.exports = { }, read() { - debug('validate read'); + debug("validate read"); return this.browse(...arguments); }, @@ -134,15 +140,21 @@ module.exports = { * @param {import('@tryghost/api-framework').Frame} frame */ add(apiConfig, frame) { - debug('validate add'); + debug("validate add"); // NOTE: this block should be removed completely once JSON Schema validations // are introduced for all of the endpoints - if (!['posts', 'tags'].includes(apiConfig.docName)) { - if (_.isEmpty(frame.data) || _.isEmpty(frame.data[apiConfig.docName]) || _.isEmpty(frame.data[apiConfig.docName][0])) { - return Promise.reject(new BadRequestError({ - message: tpl(messages.noRootKeyProvided, {docName: apiConfig.docName}) - })); + if (!["posts", "tags"].includes(apiConfig.docName)) { + if ( + _.isEmpty(frame.data) || + _.isEmpty(frame.data[apiConfig.docName]) || + _.isEmpty(frame.data[apiConfig.docName][0]) + ) { + return Promise.reject( + new BadRequestError({ + message: tpl(messages.noRootKeyProvided, { docName: apiConfig.docName }), + }), + ); } } @@ -159,21 +171,25 @@ module.exports = { }); if (missedDataProperties.length) { - return Promise.reject(new ValidationError({ - message: tpl(messages.validationFailed, { - validationName: 'FieldIsRequired', - key: JSON.stringify(missedDataProperties) - }) - })); + return Promise.reject( + new ValidationError({ + message: tpl(messages.validationFailed, { + validationName: "FieldIsRequired", + key: JSON.stringify(missedDataProperties), + }), + }), + ); } if (nilDataProperties.length) { - return Promise.reject(new ValidationError({ - message: tpl(messages.validationFailed, { - validationName: 'FieldIsInvalid', - key: JSON.stringify(nilDataProperties) - }) - })); + return Promise.reject( + new ValidationError({ + message: tpl(messages.validationFailed, { + validationName: "FieldIsInvalid", + key: JSON.stringify(nilDataProperties), + }), + }), + ); } } }, @@ -183,7 +199,7 @@ module.exports = { * @param {import('@tryghost/api-framework').Frame} frame */ edit(apiConfig, frame) { - debug('validate edit'); + debug("validate edit"); const result = this.add(...arguments); if (result instanceof Promise) { @@ -194,33 +210,38 @@ module.exports = { // are introduced for all of the endpoints. `id` property is currently // stripped from the request body and only the one provided in `options` // is used in later logic - if (!['posts', 'tags'].includes(apiConfig.docName)) { - if (frame.options.id && frame.data[apiConfig.docName][0].id - && frame.options.id !== frame.data[apiConfig.docName][0].id) { - return Promise.reject(new BadRequestError({ - message: tpl(messages.invalidIdProvided) - })); + if (!["posts", "tags"].includes(apiConfig.docName)) { + if ( + frame.options.id && + frame.data[apiConfig.docName][0].id && + frame.options.id !== frame.data[apiConfig.docName][0].id + ) { + return Promise.reject( + new BadRequestError({ + message: tpl(messages.invalidIdProvided), + }), + ); } } }, changePassword() { - debug('validate changePassword'); + debug("validate changePassword"); return this.add(...arguments); }, resetPassword() { - debug('validate resetPassword'); + debug("validate resetPassword"); return this.add(...arguments); }, setup() { - debug('validate setup'); + debug("validate setup"); return this.add(...arguments); }, publish() { - debug('validate schedule'); + debug("validate schedule"); return this.browse(...arguments); - } + }, }; diff --git a/packages/api-framework/lib/validators/input/index.js b/packages/api-framework/lib/validators/input/index.js index 7b25869f4..de014d607 100644 --- a/packages/api-framework/lib/validators/input/index.js +++ b/packages/api-framework/lib/validators/input/index.js @@ -1,5 +1,5 @@ module.exports = { get all() { - return require('./all'); - } + return require("./all"); + }, }; diff --git a/packages/api-framework/package.json b/packages/api-framework/package.json index 49823b5a1..e00da77ce 100644 --- a/packages/api-framework/package.json +++ b/packages/api-framework/package.json @@ -1,16 +1,20 @@ { "name": "@tryghost/api-framework", "version": "3.0.3", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/api-framework" }, - "author": "Ghost Foundation", + "files": [ + "index.js", + "lib" + ], + "main": "index.js", "publishConfig": { "access": "public" }, - "main": "index.js", "scripts": { "dev": "echo \"Implement me!\"", "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", @@ -20,13 +24,6 @@ "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", "lint:eslint": "yarn lint:code && yarn lint:test" }, - "files": [ - "index.js", - "lib" - ], - "devDependencies": { - "sinon": "21.0.3" - }, "dependencies": { "@tryghost/debug": "^2.0.3", "@tryghost/errors": "^3.0.3", @@ -34,5 +31,8 @@ "@tryghost/tpl": "^2.0.3", "@tryghost/validator": "^2.0.3", "lodash": "4.17.23" + }, + "devDependencies": { + "sinon": "21.0.3" } } diff --git a/packages/api-framework/test/.eslintrc.js b/packages/api-framework/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/api-framework/test/.eslintrc.js +++ b/packages/api-framework/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/api-framework/test/api-framework.test.js b/packages/api-framework/test/api-framework.test.js index 8c7a9c908..ba812bb15 100644 --- a/packages/api-framework/test/api-framework.test.js +++ b/packages/api-framework/test/api-framework.test.js @@ -1,8 +1,8 @@ -const assert = require('node:assert/strict'); +const assert = require("node:assert/strict"); -describe('api-framework module exports', function () { - it('exposes all lazy getters', function () { - const apiFramework = require('../lib/api-framework'); +describe("api-framework module exports", function () { + it("exposes all lazy getters", function () { + const apiFramework = require("../lib/api-framework"); assert.ok(apiFramework.headers); assert.ok(apiFramework.http); @@ -13,8 +13,8 @@ describe('api-framework module exports', function () { assert.ok(apiFramework.utils); }); - it('exposes serializer output module', function () { - const serializers = require('../lib/serializers'); + it("exposes serializer output module", function () { + const serializers = require("../lib/serializers"); assert.deepEqual(serializers.output, {}); }); }); diff --git a/packages/api-framework/test/frame.test.js b/packages/api-framework/test/frame.test.js index 8a42a4c0b..17d7ad0c8 100644 --- a/packages/api-framework/test/frame.test.js +++ b/packages/api-framework/test/frame.test.js @@ -1,30 +1,30 @@ -const assert = require('node:assert/strict'); -const shared = require('../'); +const assert = require("node:assert/strict"); +const shared = require("../"); -describe('Frame', function () { - it('constructor', function () { +describe("Frame", function () { + it("constructor", function () { const frame = new shared.Frame(); assert.deepEqual(Object.keys(frame), [ - 'original', - 'options', - 'data', - 'user', - 'file', - 'files', - 'apiType', - 'docName', - 'method', - 'response' + "original", + "options", + "data", + "user", + "file", + "files", + "apiType", + "docName", + "method", + "response", ]); }); - describe('fn: configure', function () { - it('no transform', function () { + describe("fn: configure", function () { + it("no transform", function () { const original = { - context: {user: 'id'}, - body: {posts: []}, - params: {id: 'id'}, - query: {include: 'tags', filter: 'type:post', soup: 'yumyum'} + context: { user: "id" }, + body: { posts: [] }, + params: { id: "id" }, + query: { include: "tags", filter: "type:post", soup: "yumyum" }, }; const frame = new shared.Frame(original); @@ -40,18 +40,18 @@ describe('Frame', function () { assert.ok(frame.data.posts); }); - it('transform with query', function () { + it("transform with query", function () { const original = { - context: {user: 'id'}, - body: {posts: []}, - params: {id: 'id'}, - query: {include: 'tags', filter: 'type:post', soup: 'yumyum'} + context: { user: "id" }, + body: { posts: [] }, + params: { id: "id" }, + query: { include: "tags", filter: "type:post", soup: "yumyum" }, }; const frame = new shared.Frame(original); frame.configure({ - options: ['include', 'filter', 'id'] + options: ["include", "filter", "id"], }); assert.ok(frame.options.context.user); @@ -63,37 +63,37 @@ describe('Frame', function () { assert.ok(frame.data.posts); }); - it('transform', function () { + it("transform", function () { const original = { - context: {user: 'id'}, + context: { user: "id" }, options: { - slug: 'slug' - } + slug: "slug", + }, }; const frame = new shared.Frame(original); frame.configure({ - options: ['include', 'filter', 'slug'] + options: ["include", "filter", "slug"], }); assert.ok(frame.options.context.user); assert.ok(frame.options.slug); }); - it('transform with data', function () { + it("transform with data", function () { const original = { - context: {user: 'id'}, + context: { user: "id" }, options: { - id: 'id' + id: "id", }, - body: {} + body: {}, }; const frame = new shared.Frame(original); frame.configure({ - data: ['id'] + data: ["id"], }); assert.ok(frame.options.context.user); @@ -101,42 +101,42 @@ describe('Frame', function () { assert.ok(frame.data.id); }); - it('supports options/data selectors as functions', function () { + it("supports options/data selectors as functions", function () { const original = { - context: {user: 'id'}, - query: {include: 'tags'}, - params: {slug: 'abc'}, - options: {id: 'id'} + context: { user: "id" }, + query: { include: "tags" }, + params: { slug: "abc" }, + options: { id: "id" }, }; const frame = new shared.Frame(original); frame.configure({ options() { - return ['include', 'slug']; + return ["include", "slug"]; }, data() { - return ['slug', 'id']; - } + return ["slug", "id"]; + }, }); - assert.equal(frame.options.include, 'tags'); - assert.equal(frame.options.slug, 'abc'); - assert.equal(frame.data.slug, 'abc'); - assert.equal(frame.data.id, 'id'); + assert.equal(frame.options.include, "tags"); + assert.equal(frame.options.slug, "abc"); + assert.equal(frame.data.slug, "abc"); + assert.equal(frame.data.id, "id"); }); }); - describe('headers', function () { - it('sets and returns copied headers', function () { + describe("headers", function () { + it("sets and returns copied headers", function () { const frame = new shared.Frame(); - frame.setHeader('X-Test', '1'); + frame.setHeader("X-Test", "1"); const headers = frame.getHeaders(); - assert.deepEqual(headers, {'X-Test': '1'}); + assert.deepEqual(headers, { "X-Test": "1" }); - headers['X-Test'] = '2'; - assert.deepEqual(frame.getHeaders(), {'X-Test': '1'}); + headers["X-Test"] = "2"; + assert.deepEqual(frame.getHeaders(), { "X-Test": "1" }); }); }); }); diff --git a/packages/api-framework/test/headers.test.js b/packages/api-framework/test/headers.test.js index 250a53c2c..a045f84ab 100644 --- a/packages/api-framework/test/headers.test.js +++ b/packages/api-framework/test/headers.test.js @@ -1,242 +1,261 @@ -const assert = require('node:assert/strict'); -const shared = require('../'); -const Frame = require('../lib/Frame'); +const assert = require("node:assert/strict"); +const shared = require("../"); +const Frame = require("../lib/Frame"); -describe('Headers', function () { - it('empty headers config', function () { +describe("Headers", function () { + it("empty headers config", function () { return shared.headers.get({}, {}, new Frame()).then((result) => { assert.deepEqual(result, {}); }); }); - describe('config.disposition', function () { - it('json', function () { - return shared.headers.get({}, {disposition: {type: 'json', value: 'value'}}, new Frame()) + describe("config.disposition", function () { + it("json", function () { + return shared.headers + .get({}, { disposition: { type: "json", value: "value" } }, new Frame()) .then((result) => { assert.deepEqual(result, { - 'Content-Disposition': 'Attachment; filename="value"', - 'Content-Type': 'application/json', - 'Content-Length': 2 + "Content-Disposition": 'Attachment; filename="value"', + "Content-Type": "application/json", + "Content-Length": 2, }); }); }); - it('csv', function () { - return shared.headers.get({}, {disposition: {type: 'csv', value: 'my.csv'}}, new Frame()) + it("csv", function () { + return shared.headers + .get({}, { disposition: { type: "csv", value: "my.csv" } }, new Frame()) .then((result) => { assert.deepEqual(result, { - 'Content-Disposition': 'Attachment; filename="my.csv"', - 'Content-Type': 'text/csv' + "Content-Disposition": 'Attachment; filename="my.csv"', + "Content-Type": "text/csv", }); }); }); - it('csv with function', async function () { - const result = await shared.headers.get({}, { - disposition: { - type: 'csv', - value() { - // pretend we're doing some dynamic filename logic in this function - const filename = `awesome-data-2022-08-01.csv`; - return filename; - } - } - }, new Frame()); + it("csv with function", async function () { + const result = await shared.headers.get( + {}, + { + disposition: { + type: "csv", + value() { + // pretend we're doing some dynamic filename logic in this function + const filename = `awesome-data-2022-08-01.csv`; + return filename; + }, + }, + }, + new Frame(), + ); assert.deepEqual(result, { - 'Content-Disposition': 'Attachment; filename="awesome-data-2022-08-01.csv"', - 'Content-Type': 'text/csv' + "Content-Disposition": 'Attachment; filename="awesome-data-2022-08-01.csv"', + "Content-Type": "text/csv", }); }); - it('file', async function () { - const result = await shared.headers.get({}, {disposition: {type: 'file', value: 'my.txt'}}, new Frame()); + it("file", async function () { + const result = await shared.headers.get( + {}, + { disposition: { type: "file", value: "my.txt" } }, + new Frame(), + ); assert.deepEqual(result, { - 'Content-Disposition': 'Attachment; filename="my.txt"' + "Content-Disposition": 'Attachment; filename="my.txt"', }); }); - it('file with function', async function () { - const result = await shared.headers.get({}, { - disposition: { - type: 'file', - value() { - // pretend we're doing some dynamic filename logic in this function - const filename = `awesome-data-2022-08-01.txt`; - return filename; - } - } - }, new Frame()); + it("file with function", async function () { + const result = await shared.headers.get( + {}, + { + disposition: { + type: "file", + value() { + // pretend we're doing some dynamic filename logic in this function + const filename = `awesome-data-2022-08-01.txt`; + return filename; + }, + }, + }, + new Frame(), + ); assert.deepEqual(result, { - 'Content-Disposition': 'Attachment; filename="awesome-data-2022-08-01.txt"' + "Content-Disposition": 'Attachment; filename="awesome-data-2022-08-01.txt"', }); }); - it('yaml', function () { - return shared.headers.get('yaml file', {disposition: {type: 'yaml', value: 'my.yaml'}}, new Frame()) + it("yaml", function () { + return shared.headers + .get("yaml file", { disposition: { type: "yaml", value: "my.yaml" } }, new Frame()) .then((result) => { assert.deepEqual(result, { - 'Content-Disposition': 'Attachment; filename="my.yaml"', - 'Content-Type': 'application/yaml', - 'Content-Length': 11 + "Content-Disposition": 'Attachment; filename="my.yaml"', + "Content-Type": "application/yaml", + "Content-Length": 11, }); }); }); }); - describe('config.cacheInvalidate', function () { - it('default', function () { - return shared.headers.get({}, {cacheInvalidate: true}, new Frame()) - .then((result) => { - assert.deepEqual(result, { - 'X-Cache-Invalidate': '/*' - }); + describe("config.cacheInvalidate", function () { + it("default", function () { + return shared.headers.get({}, { cacheInvalidate: true }, new Frame()).then((result) => { + assert.deepEqual(result, { + "X-Cache-Invalidate": "/*", }); + }); }); - it('custom value', function () { - return shared.headers.get({}, {cacheInvalidate: {value: 'value'}}, new Frame()) + it("custom value", function () { + return shared.headers + .get({}, { cacheInvalidate: { value: "value" } }, new Frame()) .then((result) => { assert.deepEqual(result, { - 'X-Cache-Invalidate': 'value' + "X-Cache-Invalidate": "value", }); }); }); }); - describe('location header', function () { - it('adds header when all needed data is present and method is add', function () { + describe("location header", function () { + it("adds header when all needed data is present and method is add", function () { const apiResult = { - posts: [{ - id: 'id_value' - }] + posts: [ + { + id: "id_value", + }, + ], }; const apiConfigHeaders = {}; const frame = new Frame(); - frame.docName = 'posts', - frame.method = 'add', - frame.original = { - url: { - host: 'example.com', - pathname: `/api/content/posts/` - } - }; + ((frame.docName = "posts"), + (frame.method = "add"), + (frame.original = { + url: { + host: "example.com", + pathname: `/api/content/posts/`, + }, + })); - return shared.headers.get(apiResult, apiConfigHeaders, frame) - .then((result) => { - assert.deepEqual(result, { - // NOTE: the backslash in the end is important to avoid unecessary 301s using the header - Location: 'https://example.com/api/content/posts/id_value/' - }); + return shared.headers.get(apiResult, apiConfigHeaders, frame).then((result) => { + assert.deepEqual(result, { + // NOTE: the backslash in the end is important to avoid unecessary 301s using the header + Location: "https://example.com/api/content/posts/id_value/", }); + }); }); - it('adds header when a location resolver is provided', function () { + it("adds header when a location resolver is provided", function () { const apiResult = { - posts: [{ - id: 'id_value' - }] + posts: [ + { + id: "id_value", + }, + ], }; - const resolvedLocationUrl = 'resolved location'; + const resolvedLocationUrl = "resolved location"; const apiConfigHeaders = { location: { resolve() { return resolvedLocationUrl; - } - } + }, + }, }; const frame = new Frame(); - frame.docName = 'posts'; - frame.method = 'copy'; + frame.docName = "posts"; + frame.method = "copy"; frame.original = { url: { - host: 'example.com', - pathname: `/api/content/posts/existing_post_id_value/copy` - } + host: "example.com", + pathname: `/api/content/posts/existing_post_id_value/copy`, + }, }; - return shared.headers.get(apiResult, apiConfigHeaders, frame) - .then((result) => { - assert.deepEqual(result, { - Location: resolvedLocationUrl - }); + return shared.headers.get(apiResult, apiConfigHeaders, frame).then((result) => { + assert.deepEqual(result, { + Location: resolvedLocationUrl, }); + }); }); - it('respects HTTP redirects', async function () { + it("respects HTTP redirects", async function () { const apiResult = { - posts: [{ - id: 'id_value' - }] + posts: [ + { + id: "id_value", + }, + ], }; const apiConfigHeaders = {}; const frame = new Frame(); - frame.docName = 'posts'; - frame.method = 'add'; + frame.docName = "posts"; + frame.method = "add"; frame.original = { url: { - host: 'example.com', + host: "example.com", pathname: `/api/content/posts/`, - secure: false - } + secure: false, + }, }; const result = await shared.headers.get(apiResult, apiConfigHeaders, frame); assert.deepEqual(result, { // NOTE: the backslash in the end is important to avoid unecessary 301s using the header - Location: 'http://example.com/api/content/posts/id_value/' + Location: "http://example.com/api/content/posts/id_value/", }); }); - it('adds and resolves header to correct url when pathname does not contain backslash in the end', function () { + it("adds and resolves header to correct url when pathname does not contain backslash in the end", function () { const apiResult = { - posts: [{ - id: 'id_value' - }] + posts: [ + { + id: "id_value", + }, + ], }; const apiConfigHeaders = {}; const frame = new Frame(); - frame.docName = 'posts'; - frame.method = 'add'; + frame.docName = "posts"; + frame.method = "add"; frame.original = { url: { - host: 'example.com', - pathname: `/api/content/posts` - } + host: "example.com", + pathname: `/api/content/posts`, + }, }; - return shared.headers.get(apiResult, apiConfigHeaders, frame) - .then((result) => { - assert.deepEqual(result, { - // NOTE: the backslash in the end is important to avoid unecessary 301s using the header - Location: 'https://example.com/api/content/posts/id_value/' - }); + return shared.headers.get(apiResult, apiConfigHeaders, frame).then((result) => { + assert.deepEqual(result, { + // NOTE: the backslash in the end is important to avoid unecessary 301s using the header + Location: "https://example.com/api/content/posts/id_value/", }); + }); }); - it('does not add header when missing result values', function () { + it("does not add header when missing result values", function () { const apiResult = {}; const apiConfigHeaders = {}; const frame = new Frame(); - frame.docName = 'posts'; - frame.method = 'add'; + frame.docName = "posts"; + frame.method = "add"; frame.original = { url: { - host: 'example.com', - pathname: `/api/content/posts/` - } + host: "example.com", + pathname: `/api/content/posts/`, + }, }; - return shared.headers.get(apiResult, apiConfigHeaders, frame) - .then((result) => { - assert.deepEqual(result, {}); - }); + return shared.headers.get(apiResult, apiConfigHeaders, frame).then((result) => { + assert.deepEqual(result, {}); + }); }); }); }); diff --git a/packages/api-framework/test/http.test.js b/packages/api-framework/test/http.test.js index 93b8ec6fd..7ba1f793a 100644 --- a/packages/api-framework/test/http.test.js +++ b/packages/api-framework/test/http.test.js @@ -1,8 +1,8 @@ -const assert = require('node:assert/strict'); -const sinon = require('sinon'); -const shared = require('../'); +const assert = require("node:assert/strict"); +const sinon = require("sinon"); +const shared = require("../"); -describe('HTTP', function () { +describe("HTTP", function () { let req; let res; let next; @@ -13,59 +13,57 @@ describe('HTTP', function () { next = sinon.stub(); req.body = { - a: 'a' + a: "a", }; req.vhost = { - host: 'example.com' + host: "example.com", }; - req.get = sinon.stub().returns('fallback.example.com'); - req.originalUrl = '/ghost/api/content/posts/'; + req.get = sinon.stub().returns("fallback.example.com"); + req.originalUrl = "/ghost/api/content/posts/"; req.secure = true; - req.url = 'https://example.com/ghost/api/content/', - - res.status = sinon.stub(); + ((req.url = "https://example.com/ghost/api/content/"), (res.status = sinon.stub())); res.json = sinon.stub(); res.set = (headers) => { res.headers = headers; }; res.send = sinon.stub(); - sinon.stub(shared.headers, 'get').resolves(); + sinon.stub(shared.headers, "get").resolves(); }); afterEach(function () { sinon.restore(); }); - it('check options', function () { + it("check options", function () { const apiImpl = sinon.stub().resolves(); shared.http(apiImpl)(req, res, next); assert.deepEqual(Object.keys(apiImpl.args[0][0]), [ - 'original', - 'options', - 'data', - 'user', - 'file', - 'files', - 'apiType', - 'docName', - 'method', - 'response' + "original", + "options", + "data", + "user", + "file", + "files", + "apiType", + "docName", + "method", + "response", ]); - assert.deepEqual(apiImpl.args[0][0].data, {a: 'a'}); + assert.deepEqual(apiImpl.args[0][0].data, { a: "a" }); assert.deepEqual(apiImpl.args[0][0].options, { context: { api_key: null, integration: null, user: null, - member: null - } + member: null, + }, }); }); - it('api response is fn', async function () { + it("api response is fn", async function () { await new Promise((resolve) => { const response = sinon.stub().callsFake(function (_req, _res, _next) { assert.ok(_req); @@ -81,9 +79,9 @@ describe('HTTP', function () { }); }); - it('api response is fn (data)', async function () { + it("api response is fn (data)", async function () { await new Promise((resolve) => { - const apiImpl = sinon.stub().resolves('data'); + const apiImpl = sinon.stub().resolves("data"); next.callsFake(resolve); @@ -98,22 +96,22 @@ describe('HTTP', function () { }); }); - it('handles api key, user and plain text response', async function () { + it("handles api key, user and plain text response", async function () { await new Promise((resolve) => { req.vhost = null; - req.user = {id: 'user-id'}; + req.user = { id: "user-id" }; req.api_key = { get(key) { return { - id: 'api-key-id', - type: 'admin', - integration_id: 'integration-id' + id: "api-key-id", + type: "admin", + integration_id: "integration-id", }[key]; - } + }, }; - const apiImpl = sinon.stub().resolves('plain body'); - apiImpl.response = {format: 'plain'}; + const apiImpl = sinon.stub().resolves("plain body"); + apiImpl.response = { format: "plain" }; apiImpl.statusCode = 201; res.send.callsFake(() => { @@ -122,9 +120,9 @@ describe('HTTP', function () { assert.equal(res.json.called, false); const frame = apiImpl.args[0][0]; - assert.equal(frame.options.context.api_key.id, 'api-key-id'); - assert.equal(frame.options.context.integration.id, 'integration-id'); - assert.equal(frame.options.context.user, 'user-id'); + assert.equal(frame.options.context.api_key.id, "api-key-id"); + assert.equal(frame.options.context.integration.id, "integration-id"); + assert.equal(frame.options.context.user, "user-id"); resolve(); }); @@ -132,14 +130,14 @@ describe('HTTP', function () { }); }); - it('supports async response format and statusCode function', async function () { + it("supports async response format and statusCode function", async function () { await new Promise((resolve) => { - const apiImpl = sinon.stub().resolves({ok: true}); + const apiImpl = sinon.stub().resolves({ ok: true }); apiImpl.statusCode = sinon.stub().returns(204); apiImpl.response = { format() { - return Promise.resolve('plain'); - } + return Promise.resolve("plain"); + }, }; res.send.callsFake(() => { @@ -152,13 +150,13 @@ describe('HTTP', function () { }); }); - it('supports sync response format function', async function () { + it("supports sync response format function", async function () { await new Promise((resolve) => { - const apiImpl = sinon.stub().resolves('plain body'); + const apiImpl = sinon.stub().resolves("plain body"); apiImpl.response = { format() { - return 'plain'; - } + return "plain"; + }, }; res.send.callsFake(() => { @@ -171,16 +169,16 @@ describe('HTTP', function () { }); }); - it('passes errors to next with frame options', async function () { + it("passes errors to next with frame options", async function () { await new Promise((resolve) => { - const error = new Error('failure'); + const error = new Error("failure"); const apiImpl = sinon.stub().rejects(error); next.callsFake((err) => { assert.equal(err, error); assert.deepEqual(req.frameOptions, { docName: null, - method: null + method: null, }); resolve(); }); @@ -189,15 +187,15 @@ describe('HTTP', function () { }); }); - it('uses req.url pathname when originalUrl is missing', async function () { + it("uses req.url pathname when originalUrl is missing", async function () { await new Promise((resolve) => { req.originalUrl = undefined; - req.url = '/ghost/api/content/posts/?include=authors'; + req.url = "/ghost/api/content/posts/?include=authors"; const apiImpl = sinon.stub().resolves({}); res.json.callsFake(() => { const frame = apiImpl.args[0][0]; - assert.equal(frame.original.url.pathname, '/ghost/api/content/posts/'); + assert.equal(frame.original.url.pathname, "/ghost/api/content/posts/"); resolve(); }); diff --git a/packages/api-framework/test/pipeline.test.js b/packages/api-framework/test/pipeline.test.js index b0e7e88e8..6141ff468 100644 --- a/packages/api-framework/test/pipeline.test.js +++ b/packages/api-framework/test/pipeline.test.js @@ -1,133 +1,149 @@ -const errors = require('@tryghost/errors'); -const assert = require('node:assert/strict'); -const sinon = require('sinon'); -const shared = require('../'); +const errors = require("@tryghost/errors"); +const assert = require("node:assert/strict"); +const sinon = require("sinon"); +const shared = require("../"); -describe('Pipeline', function () { +describe("Pipeline", function () { afterEach(function () { sinon.restore(); }); - describe('stages', function () { - describe('validation', function () { - describe('input', function () { + describe("stages", function () { + describe("validation", function () { + describe("input", function () { beforeEach(function () { - sinon.stub(shared.validators.handle, 'input').resolves(); + sinon.stub(shared.validators.handle, "input").resolves(); }); - it('do it yourself', function () { + it("do it yourself", function () { const apiUtils = {}; const apiConfig = {}; const apiImpl = { - validation: sinon.stub().resolves('response') + validation: sinon.stub().resolves("response"), }; const frame = {}; - return shared.pipeline.STAGES.validation.input(apiUtils, apiConfig, apiImpl, frame) + return shared.pipeline.STAGES.validation + .input(apiUtils, apiConfig, apiImpl, frame) .then((response) => { - assert.equal(response, 'response'); + assert.equal(response, "response"); assert.equal(apiImpl.validation.calledOnce, true); assert.equal(shared.validators.handle.input.called, false); }); }); - it('default', function () { + it("default", function () { const apiUtils = { validators: { input: { - posts: {} - } - } + posts: {}, + }, + }, }; const apiConfig = { - docName: 'posts' + docName: "posts", }; const apiImpl = { - options: ['include'], + options: ["include"], validation: { options: { include: { - required: true - } - } - } + required: true, + }, + }, + }, }; const frame = { - options: {} + options: {}, }; - return shared.pipeline.STAGES.validation.input(apiUtils, apiConfig, apiImpl, frame) + return shared.pipeline.STAGES.validation + .input(apiUtils, apiConfig, apiImpl, frame) .then(() => { assert.equal(shared.validators.handle.input.calledOnce, true); - assert.equal(shared.validators.handle.input.calledWith( - { - docName: 'posts', - options: { - include: { - required: true - } - } - }, - { - posts: {} - }, - { - options: {} - }), true); + assert.equal( + shared.validators.handle.input.calledWith( + { + docName: "posts", + options: { + include: { + required: true, + }, + }, + }, + { + posts: {}, + }, + { + options: {}, + }, + ), + true, + ); }); }); }); }); - describe('serialisation', function () { - it('input calls shared serializer input handler', function () { - sinon.stub(shared.serializers.handle, 'input').resolves(); + describe("serialisation", function () { + it("input calls shared serializer input handler", function () { + sinon.stub(shared.serializers.handle, "input").resolves(); - const apiUtils = {serializers: {input: {posts: {}}}}; - const apiConfig = {docName: 'posts', method: 'browse'}; - const apiImpl = {data: ['id']}; + const apiUtils = { serializers: { input: { posts: {} } } }; + const apiConfig = { docName: "posts", method: "browse" }; + const apiImpl = { data: ["id"] }; const frame = {}; - return shared.pipeline.STAGES.serialisation.input(apiUtils, apiConfig, apiImpl, frame) + return shared.pipeline.STAGES.serialisation + .input(apiUtils, apiConfig, apiImpl, frame) .then(() => { assert.equal(shared.serializers.handle.input.calledOnce, true); assert.deepEqual(shared.serializers.handle.input.args[0][0], { - data: ['id'], - docName: 'posts', - method: 'browse' + data: ["id"], + docName: "posts", + method: "browse", }); }); }); - it('output calls shared serializer output handler', function () { - sinon.stub(shared.serializers.handle, 'output').resolves(); + it("output calls shared serializer output handler", function () { + sinon.stub(shared.serializers.handle, "output").resolves(); - const apiUtils = {serializers: {output: {posts: {}}}}; - const apiConfig = {docName: 'posts', method: 'browse'}; + const apiUtils = { serializers: { output: { posts: {} } } }; + const apiConfig = { docName: "posts", method: "browse" }; const apiImpl = {}; const frame = {}; - const response = [{id: '1'}]; + const response = [{ id: "1" }]; - return shared.pipeline.STAGES.serialisation.output(response, apiUtils, apiConfig, apiImpl, frame) + return shared.pipeline.STAGES.serialisation + .output(response, apiUtils, apiConfig, apiImpl, frame) .then(() => { - assert.equal(shared.serializers.handle.output.calledOnceWithExactly(response, apiConfig, apiUtils.serializers.output, frame), true); + assert.equal( + shared.serializers.handle.output.calledOnceWithExactly( + response, + apiConfig, + apiUtils.serializers.output, + frame, + ), + true, + ); }); }); }); - describe('permissions', function () { + describe("permissions", function () { let apiUtils; beforeEach(function () { apiUtils = { permissions: { - handle: sinon.stub().resolves() - } + handle: sinon.stub().resolves(), + }, }; }); - it('key is missing', function () { + it("key is missing", function () { const apiConfig = {}; const apiImpl = {}; const frame = {}; @@ -140,94 +156,103 @@ describe('Pipeline', function () { }); }); - it('do it yourself', function () { + it("do it yourself", function () { const apiConfig = {}; const apiImpl = { - permissions: sinon.stub().resolves('lol') + permissions: sinon.stub().resolves("lol"), }; const frame = {}; - return shared.pipeline.STAGES.permissions(apiUtils, apiConfig, apiImpl, frame) - .then((response) => { - assert.equal(response, 'lol'); + return shared.pipeline.STAGES.permissions(apiUtils, apiConfig, apiImpl, frame).then( + (response) => { + assert.equal(response, "lol"); assert.equal(apiImpl.permissions.calledOnce, true); assert.equal(apiUtils.permissions.handle.called, false); - }); + }, + ); }); - it('skip stage', function () { + it("skip stage", function () { const apiConfig = {}; const apiImpl = { - permissions: false + permissions: false, }; const frame = {}; - return shared.pipeline.STAGES.permissions(apiUtils, apiConfig, apiImpl, frame) - .then(() => { + return shared.pipeline.STAGES.permissions(apiUtils, apiConfig, apiImpl, frame).then( + () => { assert.equal(apiUtils.permissions.handle.called, false); - }); + }, + ); }); - it('default', function () { + it("default", function () { const apiConfig = {}; const apiImpl = { - permissions: true + permissions: true, }; const frame = {}; - return shared.pipeline.STAGES.permissions(apiUtils, apiConfig, apiImpl, frame) - .then(() => { + return shared.pipeline.STAGES.permissions(apiUtils, apiConfig, apiImpl, frame).then( + () => { assert.equal(apiUtils.permissions.handle.calledOnce, true); - }); + }, + ); }); - it('with permission config', function () { + it("with permission config", function () { const apiConfig = { - docName: 'posts' + docName: "posts", }; const apiImpl = { permissions: { - unsafeAttrs: ['test'] - } + unsafeAttrs: ["test"], + }, }; const frame = { - options: {} + options: {}, }; - return shared.pipeline.STAGES.permissions(apiUtils, apiConfig, apiImpl, frame) - .then(() => { + return shared.pipeline.STAGES.permissions(apiUtils, apiConfig, apiImpl, frame).then( + () => { assert.equal(apiUtils.permissions.handle.calledOnce, true); - assert.equal(apiUtils.permissions.handle.calledWith( - { - docName: 'posts', - unsafeAttrs: ['test'] - }, - { - options: {} - }), true); - }); + assert.equal( + apiUtils.permissions.handle.calledWith( + { + docName: "posts", + unsafeAttrs: ["test"], + }, + { + options: {}, + }, + ), + true, + ); + }, + ); }); - it('runs permission before hook', function () { + it("runs permission before hook", function () { const before = sinon.stub().resolves(); const apiConfig = {}; const apiImpl = { permissions: { - before - } + before, + }, }; const frame = {}; - return shared.pipeline.STAGES.permissions(apiUtils, apiConfig, apiImpl, frame) - .then(() => { + return shared.pipeline.STAGES.permissions(apiUtils, apiConfig, apiImpl, frame).then( + () => { assert.equal(before.calledOnceWithExactly(frame), true); assert.equal(apiUtils.permissions.handle.calledOnce, true); - }); + }, + ); }); }); - describe('query', function () { - it('throws when query method is missing', function () { + describe("query", function () { + it("throws when query method is missing", function () { return shared.pipeline.STAGES.query({}, {}, {}, {}) .then(Promise.reject) .catch((err) => { @@ -235,47 +260,46 @@ describe('Pipeline', function () { }); }); - it('runs query when configured', function () { - const query = sinon.stub().resolves('result'); + it("runs query when configured", function () { + const query = sinon.stub().resolves("result"); const frame = {}; - return shared.pipeline.STAGES.query({}, {}, {query}, frame) - .then((result) => { - assert.equal(result, 'result'); - assert.equal(query.calledOnceWithExactly(frame), true); - }); + return shared.pipeline.STAGES.query({}, {}, { query }, frame).then((result) => { + assert.equal(result, "result"); + assert.equal(query.calledOnceWithExactly(frame), true); + }); }); }); }); - describe('pipeline', function () { + describe("pipeline", function () { beforeEach(function () { - sinon.stub(shared.pipeline.STAGES.validation, 'input'); - sinon.stub(shared.pipeline.STAGES.serialisation, 'input'); - sinon.stub(shared.pipeline.STAGES.serialisation, 'output'); - sinon.stub(shared.pipeline.STAGES, 'permissions'); - sinon.stub(shared.pipeline.STAGES, 'query'); + sinon.stub(shared.pipeline.STAGES.validation, "input"); + sinon.stub(shared.pipeline.STAGES.serialisation, "input"); + sinon.stub(shared.pipeline.STAGES.serialisation, "output"); + sinon.stub(shared.pipeline.STAGES, "permissions"); + sinon.stub(shared.pipeline.STAGES, "query"); }); - it('ensure we receive a callable api controller fn', function () { + it("ensure we receive a callable api controller fn", function () { const apiController = { add: {}, - browse: {} + browse: {}, }; const apiUtils = {}; const result = shared.pipeline(apiController, apiUtils); - assert.equal(typeof result, 'object'); + assert.equal(typeof result, "object"); assert.ok(result.add); assert.ok(result.browse); - assert.equal(typeof result.add, 'function'); - assert.equal(typeof result.browse, 'function'); + assert.equal(typeof result.add, "function"); + assert.equal(typeof result.browse, "function"); }); - it('call api controller fn', function () { + it("call api controller fn", function () { const apiController = { - add: {} + add: {}, }; const apiUtils = {}; @@ -284,31 +308,32 @@ describe('Pipeline', function () { shared.pipeline.STAGES.validation.input.resolves(); shared.pipeline.STAGES.serialisation.input.resolves(); shared.pipeline.STAGES.permissions.resolves(); - shared.pipeline.STAGES.query.resolves('response'); - shared.pipeline.STAGES.serialisation.output.callsFake(function (response, _apiUtils, apiConfig, apiImpl, frame) { - frame.response = response; + shared.pipeline.STAGES.query.resolves("response"); + shared.pipeline.STAGES.serialisation.output.callsFake( + function (response, _apiUtils, apiConfig, apiImpl, frame) { + frame.response = response; + }, + ); + + return result.add().then((response) => { + assert.equal(response, "response"); + + assert.equal(shared.pipeline.STAGES.validation.input.calledOnce, true); + assert.equal(shared.pipeline.STAGES.serialisation.input.calledOnce, true); + assert.equal(shared.pipeline.STAGES.permissions.calledOnce, true); + assert.equal(shared.pipeline.STAGES.query.calledOnce, true); + assert.equal(shared.pipeline.STAGES.serialisation.output.calledOnce, true); }); - - return result.add() - .then((response) => { - assert.equal(response, 'response'); - - assert.equal(shared.pipeline.STAGES.validation.input.calledOnce, true); - assert.equal(shared.pipeline.STAGES.serialisation.input.calledOnce, true); - assert.equal(shared.pipeline.STAGES.permissions.calledOnce, true); - assert.equal(shared.pipeline.STAGES.query.calledOnce, true); - assert.equal(shared.pipeline.STAGES.serialisation.output.calledOnce, true); - }); }); - it('supports data and options arguments', function () { + it("supports data and options arguments", function () { const apiController = { - docName: 'posts', + docName: "posts", add: { headers: {}, permissions: true, - query: sinon.stub().resolves('response') - } + query: sinon.stub().resolves("response"), + }, }; const apiUtils = {}; @@ -317,27 +342,30 @@ describe('Pipeline', function () { shared.pipeline.STAGES.validation.input.resolves(); shared.pipeline.STAGES.serialisation.input.resolves(); shared.pipeline.STAGES.permissions.resolves(); - shared.pipeline.STAGES.query.resolves('response'); - shared.pipeline.STAGES.serialisation.output.callsFake(function (response, _apiUtils, apiConfig, apiImpl, frame) { - frame.response = response; - }); - - return result.add({posts: [{title: 't'}]}, {context: {internal: true}}) + shared.pipeline.STAGES.query.resolves("response"); + shared.pipeline.STAGES.serialisation.output.callsFake( + function (response, _apiUtils, apiConfig, apiImpl, frame) { + frame.response = response; + }, + ); + + return result + .add({ posts: [{ title: "t" }] }, { context: { internal: true } }) .then(() => { const frame = shared.pipeline.STAGES.validation.input.args[0][3]; - assert.deepEqual(frame.data, {posts: [{title: 't'}]}); - assert.deepEqual(frame.options.context, {internal: true}); + assert.deepEqual(frame.data, { posts: [{ title: "t" }] }); + assert.deepEqual(frame.options.context, { internal: true }); }); }); - it('supports single undefined argument by defaulting options', function () { + it("supports single undefined argument by defaulting options", function () { const apiController = { - docName: 'posts', + docName: "posts", add: { headers: {}, permissions: true, - query: sinon.stub().resolves('response') - } + query: sinon.stub().resolves("response"), + }, }; const apiUtils = {}; @@ -346,79 +374,84 @@ describe('Pipeline', function () { shared.pipeline.STAGES.validation.input.resolves(); shared.pipeline.STAGES.serialisation.input.resolves(); shared.pipeline.STAGES.permissions.resolves(); - shared.pipeline.STAGES.query.resolves('response'); - shared.pipeline.STAGES.serialisation.output.callsFake(function (response, _apiUtils, apiConfig, apiImpl, frame) { - frame.response = response; + shared.pipeline.STAGES.query.resolves("response"); + shared.pipeline.STAGES.serialisation.output.callsFake( + function (response, _apiUtils, apiConfig, apiImpl, frame) { + frame.response = response; + }, + ); + + return result.add(undefined).then(() => { + const frame = shared.pipeline.STAGES.validation.input.args[0][3]; + assert.deepEqual(frame.options.context, {}); }); - - return result.add(undefined) - .then(() => { - const frame = shared.pipeline.STAGES.validation.input.args[0][3]; - assert.deepEqual(frame.options.context, {}); - }); }); - it('api controller is fn, not config', function () { + it("api controller is fn, not config", function () { const apiController = { add() { - return Promise.resolve('response'); - } + return Promise.resolve("response"); + }, }; const apiUtils = {}; const result = shared.pipeline(apiController, apiUtils); - return result.add() - .then((response) => { - assert.equal(response, 'response'); + return result.add().then((response) => { + assert.equal(response, "response"); - assert.equal(shared.pipeline.STAGES.validation.input.called, false); - assert.equal(shared.pipeline.STAGES.serialisation.input.called, false); - assert.equal(shared.pipeline.STAGES.permissions.called, false); - assert.equal(shared.pipeline.STAGES.query.called, false); - assert.equal(shared.pipeline.STAGES.serialisation.output.called, false); - }); + assert.equal(shared.pipeline.STAGES.validation.input.called, false); + assert.equal(shared.pipeline.STAGES.serialisation.input.called, false); + assert.equal(shared.pipeline.STAGES.permissions.called, false); + assert.equal(shared.pipeline.STAGES.query.called, false); + assert.equal(shared.pipeline.STAGES.serialisation.output.called, false); + }); }); - it('uses existing frame instance and generateCacheKeyData', async function () { + it("uses existing frame instance and generateCacheKeyData", async function () { const apiController = { browse: { headers: {}, permissions: true, - generateCacheKeyData: sinon.stub().resolves({custom: 'key'}), - query: sinon.stub().resolves('response') - } + generateCacheKeyData: sinon.stub().resolves({ custom: "key" }), + query: sinon.stub().resolves("response"), + }, }; const apiUtils = {}; - const result = shared.pipeline(apiController, apiUtils, 'content'); + const result = shared.pipeline(apiController, apiUtils, "content"); const frame = new shared.Frame(); shared.pipeline.STAGES.validation.input.resolves(); shared.pipeline.STAGES.serialisation.input.resolves(); shared.pipeline.STAGES.permissions.resolves(); - shared.pipeline.STAGES.query.resolves('response'); - shared.pipeline.STAGES.serialisation.output.callsFake(function (response, _apiUtils, apiConfig, apiImpl, frameArg) { - frameArg.response = response; - }); + shared.pipeline.STAGES.query.resolves("response"); + shared.pipeline.STAGES.serialisation.output.callsFake( + function (response, _apiUtils, apiConfig, apiImpl, frameArg) { + frameArg.response = response; + }, + ); const response = await result.browse(frame); - assert.equal(response, 'response'); - assert.equal(apiController.browse.generateCacheKeyData.calledOnceWithExactly(frame), true); - assert.equal(frame.apiType, 'content'); + assert.equal(response, "response"); + assert.equal( + apiController.browse.generateCacheKeyData.calledOnceWithExactly(frame), + true, + ); + assert.equal(frame.apiType, "content"); assert.equal(frame.docName, undefined); - assert.equal(frame.method, 'browse'); + assert.equal(frame.method, "browse"); }); - it('returns cached controller wrapper for same controller object', function () { + it("returns cached controller wrapper for same controller object", function () { const apiController = { - docName: 'posts', + docName: "posts", browse: { headers: {}, permissions: true, - query: sinon.stub().resolves('response') - } + query: sinon.stub().resolves("response"), + }, }; const first = shared.pipeline(apiController, {}); @@ -428,23 +461,23 @@ describe('Pipeline', function () { }); }); - describe('caching', function () { + describe("caching", function () { beforeEach(function () { - sinon.stub(shared.pipeline.STAGES.validation, 'input'); - sinon.stub(shared.pipeline.STAGES.serialisation, 'input'); - sinon.stub(shared.pipeline.STAGES.serialisation, 'output'); - sinon.stub(shared.pipeline.STAGES, 'permissions'); - sinon.stub(shared.pipeline.STAGES, 'query'); + sinon.stub(shared.pipeline.STAGES.validation, "input"); + sinon.stub(shared.pipeline.STAGES.serialisation, "input"); + sinon.stub(shared.pipeline.STAGES.serialisation, "output"); + sinon.stub(shared.pipeline.STAGES, "permissions"); + sinon.stub(shared.pipeline.STAGES, "query"); }); - it('should set a cache if configured on endpoint level', async function () { + it("should set a cache if configured on endpoint level", async function () { const apiController = { browse: { cache: { get: sinon.stub().resolves(null), - set: sinon.stub().resolves(true) - } - } + set: sinon.stub().resolves(true), + }, + }, }; const apiUtils = {}; @@ -453,14 +486,16 @@ describe('Pipeline', function () { shared.pipeline.STAGES.validation.input.resolves(); shared.pipeline.STAGES.serialisation.input.resolves(); shared.pipeline.STAGES.permissions.resolves(); - shared.pipeline.STAGES.query.resolves('response'); - shared.pipeline.STAGES.serialisation.output.callsFake(function (response, _apiUtils, apiConfig, apiImpl, frame) { - frame.response = response; - }); + shared.pipeline.STAGES.query.resolves("response"); + shared.pipeline.STAGES.serialisation.output.callsFake( + function (response, _apiUtils, apiConfig, apiImpl, frame) { + frame.response = response; + }, + ); const response = await result.browse(); - assert.equal(response, 'response'); + assert.equal(response, "response"); // request went through all stages assert.equal(shared.pipeline.STAGES.validation.input.calledOnce, true); @@ -471,17 +506,17 @@ describe('Pipeline', function () { // cache was set assert.equal(apiController.browse.cache.set.calledOnce, true); - assert.equal(apiController.browse.cache.set.args[0][1], 'response'); + assert.equal(apiController.browse.cache.set.args[0][1], "response"); }); - it('should use cache if configured on endpoint level', async function () { + it("should use cache if configured on endpoint level", async function () { const apiController = { browse: { cache: { - get: sinon.stub().resolves('CACHED RESPONSE'), - set: sinon.stub().resolves(true) - } - } + get: sinon.stub().resolves("CACHED RESPONSE"), + set: sinon.stub().resolves(true), + }, + }, }; const apiUtils = {}; @@ -490,14 +525,16 @@ describe('Pipeline', function () { shared.pipeline.STAGES.validation.input.resolves(); shared.pipeline.STAGES.serialisation.input.resolves(); shared.pipeline.STAGES.permissions.resolves(); - shared.pipeline.STAGES.query.resolves('response'); - shared.pipeline.STAGES.serialisation.output.callsFake(function (response, _apiUtils, apiConfig, apiImpl, frame) { - frame.response = response; - }); + shared.pipeline.STAGES.query.resolves("response"); + shared.pipeline.STAGES.serialisation.output.callsFake( + function (response, _apiUtils, apiConfig, apiImpl, frame) { + frame.response = response; + }, + ); const response = await result.browse(); - assert.equal(response, 'CACHED RESPONSE'); + assert.equal(response, "CACHED RESPONSE"); // request went through all stages assert.equal(shared.pipeline.STAGES.validation.input.calledOnce, false); diff --git a/packages/api-framework/test/serializers/handle.test.js b/packages/api-framework/test/serializers/handle.test.js index 06f5f8d8e..52ad405a6 100644 --- a/packages/api-framework/test/serializers/handle.test.js +++ b/packages/api-framework/test/serializers/handle.test.js @@ -1,77 +1,78 @@ -const errors = require('@tryghost/errors'); -const assert = require('node:assert/strict'); -const sinon = require('sinon'); -const shared = require('../../'); +const errors = require("@tryghost/errors"); +const assert = require("node:assert/strict"); +const sinon = require("sinon"); +const shared = require("../../"); -describe('serializers/handle', function () { +describe("serializers/handle", function () { afterEach(function () { sinon.restore(); }); - describe('input', function () { - it('no api config passed', function () { - return shared.serializers.handle.input() + describe("input", function () { + it("no api config passed", function () { + return shared.serializers.handle + .input() .then(Promise.reject) .catch((err) => { assert.equal(err instanceof errors.IncorrectUsageError, true); }); }); - it('no api serializers passed', function () { - return shared.serializers.handle.input({}) + it("no api serializers passed", function () { + return shared.serializers.handle + .input({}) .then(Promise.reject) .catch((err) => { assert.equal(err instanceof errors.IncorrectUsageError, true); }); }); - it('ensure default serializers are called with apiConfig and frame', function () { + it("ensure default serializers are called with apiConfig and frame", function () { const allStub = sinon.stub(); - sinon.stub(shared.serializers.input.all, 'all').get(() => allStub); + sinon.stub(shared.serializers.input.all, "all").get(() => allStub); const apiSerializers = { all: sinon.stub().resolves(), posts: { all: sinon.stub().resolves(), - browse: sinon.stub().resolves() - } + browse: sinon.stub().resolves(), + }, }; - const apiConfig = {docName: 'posts', method: 'browse'}; + const apiConfig = { docName: "posts", method: "browse" }; const frame = {}; const stubsToCheck = [ allStub, apiSerializers.all, apiSerializers.posts.all, - apiSerializers.posts.browse + apiSerializers.posts.browse, ]; - return shared.serializers.handle.input(apiConfig, apiSerializers, frame) - .then(() => { - stubsToCheck.forEach((stub) => { - sinon.assert.calledOnceWithExactly(stub, apiConfig, frame); - }); + return shared.serializers.handle.input(apiConfig, apiSerializers, frame).then(() => { + stubsToCheck.forEach((stub) => { + sinon.assert.calledOnceWithExactly(stub, apiConfig, frame); }); + }); }); - it('ensure serializers are called with apiConfig and frame if new shared serializer is added', function () { + it("ensure serializers are called with apiConfig and frame if new shared serializer is added", function () { const allStub = sinon.stub(); const allBrowseStub = sinon.stub(); shared.serializers.input.all.browse = allBrowseStub; - sinon.stub(shared.serializers.input.all, 'all').get(() => allStub); + sinon.stub(shared.serializers.input.all, "all").get(() => allStub); const apiSerializers = { all: sinon.stub().resolves(), posts: { all: sinon.stub().resolves(), - browse: sinon.stub().resolves() - } + browse: sinon.stub().resolves(), + }, }; - const apiConfig = {docName: 'posts', method: 'browse'}; + const apiConfig = { docName: "posts", method: "browse" }; const frame = {}; const stubsToCheck = [ @@ -79,76 +80,87 @@ describe('serializers/handle', function () { allBrowseStub, apiSerializers.all, apiSerializers.posts.all, - apiSerializers.posts.browse + apiSerializers.posts.browse, ]; - return shared.serializers.handle.input(apiConfig, apiSerializers, frame) - .then(() => { - stubsToCheck.forEach((stub) => { - sinon.assert.calledOnceWithExactly(stub, apiConfig, frame); - }); - - sinon.assert.callOrder(allStub, allBrowseStub, apiSerializers.all, apiSerializers.posts.all, apiSerializers.posts.browse); + return shared.serializers.handle.input(apiConfig, apiSerializers, frame).then(() => { + stubsToCheck.forEach((stub) => { + sinon.assert.calledOnceWithExactly(stub, apiConfig, frame); }); + + sinon.assert.callOrder( + allStub, + allBrowseStub, + apiSerializers.all, + apiSerializers.posts.all, + apiSerializers.posts.browse, + ); + }); }); }); - describe('output', function () { - let apiSerializers, - response, - apiConfig, - frame; + describe("output", function () { + let apiSerializers, response, apiConfig, frame; beforeEach(function () { response = []; - apiConfig = {docName: 'posts', method: 'add'}; + apiConfig = { docName: "posts", method: "add" }; frame = {}; }); - it('no models passed', function () { + it("no models passed", function () { return shared.serializers.handle.output(null, {}, {}, {}); }); - it('no api config passed', function () { - return shared.serializers.handle.output([]) + it("no api config passed", function () { + return shared.serializers.handle + .output([]) .then(Promise.reject) .catch((err) => { assert.equal(err instanceof errors.IncorrectUsageError, true); }); }); - it('no api serializers passed', function () { - return shared.serializers.handle.output([], {}) + it("no api serializers passed", function () { + return shared.serializers.handle + .output([], {}) .then(Promise.reject) .catch((err) => { assert.equal(err instanceof errors.IncorrectUsageError, true); }); }); - describe('Specific serializers only', function () { + describe("Specific serializers only", function () { beforeEach(function () { apiSerializers = { posts: { - add: sinon.stub().resolves() + add: sinon.stub().resolves(), }, users: { - add: sinon.stub().resolves() - } + add: sinon.stub().resolves(), + }, }; }); - it('correct custom serializer is called', function () { - return shared.serializers.handle.output(response, apiConfig, apiSerializers, frame) + it("correct custom serializer is called", function () { + return shared.serializers.handle + .output(response, apiConfig, apiSerializers, frame) .then(() => { - sinon.assert.calledOnceWithExactly(apiSerializers.posts.add, response, apiConfig, frame); + sinon.assert.calledOnceWithExactly( + apiSerializers.posts.add, + response, + apiConfig, + frame, + ); sinon.assert.notCalled(apiSerializers.users.add); }); }); - it('no serializer called if there is no match', function () { - apiConfig = {docName: 'posts', method: 'idontexist'}; + it("no serializer called if there is no match", function () { + apiConfig = { docName: "posts", method: "idontexist" }; - return shared.serializers.handle.output(response, apiConfig, apiSerializers, frame) + return shared.serializers.handle + .output(response, apiConfig, apiSerializers, frame) .then(() => { sinon.assert.notCalled(apiSerializers.posts.add); sinon.assert.notCalled(apiSerializers.users.add); @@ -156,125 +168,146 @@ describe('serializers/handle', function () { }); }); - describe('Custom and global (all) serializers', function () { + describe("Custom and global (all) serializers", function () { beforeEach(function () { apiSerializers = { all: { after: sinon.stub().resolves(), - before: sinon.stub().resolves() - + before: sinon.stub().resolves(), }, posts: { add: sinon.stub().resolves(), - all: sinon.stub().resolves() - } + all: sinon.stub().resolves(), + }, }; }); - it('calls custom serializer if one exists', function () { - const stubsToCheck = [ - apiSerializers.all.before, - apiSerializers.posts.add - ]; + it("calls custom serializer if one exists", function () { + const stubsToCheck = [apiSerializers.all.before, apiSerializers.posts.add]; - return shared.serializers.handle.output(response, apiConfig, apiSerializers, frame) + return shared.serializers.handle + .output(response, apiConfig, apiSerializers, frame) .then(() => { stubsToCheck.forEach((stub) => { sinon.assert.calledOnceWithExactly(stub, response, apiConfig, frame); }); // After has a different call signature... is this a intentional? - sinon.assert.calledOnceWithExactly(apiSerializers.all.after, apiConfig, frame); - - sinon.assert.callOrder(apiSerializers.all.before, apiSerializers.posts.add, apiSerializers.all.after); + sinon.assert.calledOnceWithExactly( + apiSerializers.all.after, + apiConfig, + frame, + ); + + sinon.assert.callOrder( + apiSerializers.all.before, + apiSerializers.posts.add, + apiSerializers.all.after, + ); sinon.assert.notCalled(apiSerializers.posts.all); }); }); - it('calls all serializer if custom one does not exist', function () { - apiConfig = {docName: 'posts', method: 'idontexist'}; + it("calls all serializer if custom one does not exist", function () { + apiConfig = { docName: "posts", method: "idontexist" }; - const stubsToCheck = [ - apiSerializers.all.before, - apiSerializers.posts.all - ]; + const stubsToCheck = [apiSerializers.all.before, apiSerializers.posts.all]; - return shared.serializers.handle.output(response, apiConfig, apiSerializers, frame) + return shared.serializers.handle + .output(response, apiConfig, apiSerializers, frame) .then(() => { stubsToCheck.forEach((stub) => { sinon.assert.calledOnceWithExactly(stub, response, apiConfig, frame); }); // After has a different call signature... is this a intentional? - sinon.assert.calledOnceWithExactly(apiSerializers.all.after, apiConfig, frame); - - sinon.assert.callOrder(apiSerializers.all.before, apiSerializers.posts.all, apiSerializers.all.after); + sinon.assert.calledOnceWithExactly( + apiSerializers.all.after, + apiConfig, + frame, + ); + + sinon.assert.callOrder( + apiSerializers.all.before, + apiSerializers.posts.all, + apiSerializers.all.after, + ); sinon.assert.notCalled(apiSerializers.posts.add); }); }); }); - describe('Custom, default and global (all) serializers with no custom fallback', function () { + describe("Custom, default and global (all) serializers with no custom fallback", function () { beforeEach(function () { apiSerializers = { all: { after: sinon.stub().resolves(), - before: sinon.stub().resolves() - + before: sinon.stub().resolves(), }, default: { add: sinon.stub().resolves(), - all: sinon.stub().resolves() - + all: sinon.stub().resolves(), }, posts: { - add: sinon.stub().resolves() - } + add: sinon.stub().resolves(), + }, }; }); - it('uses best match serializer when custom match exists', function () { - const stubsToCheck = [ - apiSerializers.all.before, - apiSerializers.posts.add - ]; + it("uses best match serializer when custom match exists", function () { + const stubsToCheck = [apiSerializers.all.before, apiSerializers.posts.add]; - return shared.serializers.handle.output(response, apiConfig, apiSerializers, frame) + return shared.serializers.handle + .output(response, apiConfig, apiSerializers, frame) .then(() => { stubsToCheck.forEach((stub) => { sinon.assert.calledOnceWithExactly(stub, response, apiConfig, frame); }); // After has a different call signature... is this a intentional? - sinon.assert.calledOnceWithExactly(apiSerializers.all.after, apiConfig, frame); - - sinon.assert.callOrder(apiSerializers.all.before, apiSerializers.posts.add, apiSerializers.all.after); + sinon.assert.calledOnceWithExactly( + apiSerializers.all.after, + apiConfig, + frame, + ); + + sinon.assert.callOrder( + apiSerializers.all.before, + apiSerializers.posts.add, + apiSerializers.all.after, + ); sinon.assert.notCalled(apiSerializers.default.add); sinon.assert.notCalled(apiSerializers.default.all); }); }); - it('uses nearest fallback serializer when custom match does not exist', function () { - apiConfig = {docName: 'posts', method: 'idontexist'}; + it("uses nearest fallback serializer when custom match does not exist", function () { + apiConfig = { docName: "posts", method: "idontexist" }; - const stubsToCheck = [ - apiSerializers.all.before, - apiSerializers.default.all - ]; + const stubsToCheck = [apiSerializers.all.before, apiSerializers.default.all]; - return shared.serializers.handle.output(response, apiConfig, apiSerializers, frame) + return shared.serializers.handle + .output(response, apiConfig, apiSerializers, frame) .then(() => { stubsToCheck.forEach((stub) => { sinon.assert.calledOnceWithExactly(stub, response, apiConfig, frame); }); // After has a different call signature... is this a intentional? - sinon.assert.calledOnceWithExactly(apiSerializers.all.after, apiConfig, frame); - - sinon.assert.callOrder(apiSerializers.all.before, apiSerializers.default.all, apiSerializers.all.after); + sinon.assert.calledOnceWithExactly( + apiSerializers.all.after, + apiConfig, + frame, + ); + + sinon.assert.callOrder( + apiSerializers.all.before, + apiSerializers.default.all, + apiSerializers.all.after, + ); sinon.assert.notCalled(apiSerializers.posts.add); sinon.assert.notCalled(apiSerializers.default.add); @@ -282,42 +315,46 @@ describe('serializers/handle', function () { }); }); - describe('Custom, default and global (all) serializers with custom fallback', function () { + describe("Custom, default and global (all) serializers with custom fallback", function () { beforeEach(function () { apiSerializers = { all: { after: sinon.stub().resolves(), - before: sinon.stub().resolves() - + before: sinon.stub().resolves(), }, default: { add: sinon.stub().resolves(), - all: sinon.stub().resolves() - + all: sinon.stub().resolves(), }, posts: { add: sinon.stub().resolves(), - all: sinon.stub().resolves() - } + all: sinon.stub().resolves(), + }, }; }); - it('uses best match serializer when custom match exists', function () { - const stubsToCheck = [ - apiSerializers.all.before, - apiSerializers.posts.add - ]; + it("uses best match serializer when custom match exists", function () { + const stubsToCheck = [apiSerializers.all.before, apiSerializers.posts.add]; - return shared.serializers.handle.output(response, apiConfig, apiSerializers, frame) + return shared.serializers.handle + .output(response, apiConfig, apiSerializers, frame) .then(() => { stubsToCheck.forEach((stub) => { sinon.assert.calledOnceWithExactly(stub, response, apiConfig, frame); }); // After has a different call signature... is this a intentional? - sinon.assert.calledOnceWithExactly(apiSerializers.all.after, apiConfig, frame); - - sinon.assert.callOrder(apiSerializers.all.before, apiSerializers.posts.add, apiSerializers.all.after); + sinon.assert.calledOnceWithExactly( + apiSerializers.all.after, + apiConfig, + frame, + ); + + sinon.assert.callOrder( + apiSerializers.all.before, + apiSerializers.posts.add, + apiSerializers.all.after, + ); sinon.assert.notCalled(apiSerializers.posts.all); sinon.assert.notCalled(apiSerializers.default.add); @@ -325,24 +362,30 @@ describe('serializers/handle', function () { }); }); - it('uses nearest fallback serializer when custom match does not exist', function () { - apiConfig = {docName: 'posts', method: 'idontexist'}; + it("uses nearest fallback serializer when custom match does not exist", function () { + apiConfig = { docName: "posts", method: "idontexist" }; - const stubsToCheck = [ - apiSerializers.all.before, - apiSerializers.posts.all - ]; + const stubsToCheck = [apiSerializers.all.before, apiSerializers.posts.all]; - return shared.serializers.handle.output(response, apiConfig, apiSerializers, frame) + return shared.serializers.handle + .output(response, apiConfig, apiSerializers, frame) .then(() => { stubsToCheck.forEach((stub) => { sinon.assert.calledOnceWithExactly(stub, response, apiConfig, frame); }); // After has a different call signature... is this a intentional? - sinon.assert.calledOnceWithExactly(apiSerializers.all.after, apiConfig, frame); - - sinon.assert.callOrder(apiSerializers.all.before, apiSerializers.posts.all, apiSerializers.all.after); + sinon.assert.calledOnceWithExactly( + apiSerializers.all.after, + apiConfig, + frame, + ); + + sinon.assert.callOrder( + apiSerializers.all.before, + apiSerializers.posts.all, + apiSerializers.all.after, + ); sinon.assert.notCalled(apiSerializers.posts.add); sinon.assert.notCalled(apiSerializers.default.add); @@ -351,59 +394,70 @@ describe('serializers/handle', function () { }); }); - describe('Default and global (all) serializers work together correctly', function () { + describe("Default and global (all) serializers work together correctly", function () { beforeEach(function () { apiSerializers = { all: { after: sinon.stub().resolves(), - before: sinon.stub().resolves() - + before: sinon.stub().resolves(), }, default: { add: sinon.stub().resolves(), - all: sinon.stub().resolves() - } + all: sinon.stub().resolves(), + }, }; }); - it('correctly calls default serializer when no custom one is set', function () { - const stubsToCheck = [ - apiSerializers.all.before, - apiSerializers.default.add - ]; + it("correctly calls default serializer when no custom one is set", function () { + const stubsToCheck = [apiSerializers.all.before, apiSerializers.default.add]; - return shared.serializers.handle.output(response, apiConfig, apiSerializers, frame) + return shared.serializers.handle + .output(response, apiConfig, apiSerializers, frame) .then(() => { stubsToCheck.forEach((stub) => { sinon.assert.calledOnceWithExactly(stub, response, apiConfig, frame); }); // After has a different call signature... is this a intentional? - sinon.assert.calledOnceWithExactly(apiSerializers.all.after, apiConfig, frame); - - sinon.assert.callOrder(apiSerializers.all.before, apiSerializers.default.add, apiSerializers.all.after); + sinon.assert.calledOnceWithExactly( + apiSerializers.all.after, + apiConfig, + frame, + ); + + sinon.assert.callOrder( + apiSerializers.all.before, + apiSerializers.default.add, + apiSerializers.all.after, + ); sinon.assert.notCalled(apiSerializers.default.all); }); }); - it('correctly uses fallback serializer when there is no default match', function () { - apiConfig = {docName: 'posts', method: 'idontexist'}; + it("correctly uses fallback serializer when there is no default match", function () { + apiConfig = { docName: "posts", method: "idontexist" }; - const stubsToCheck = [ - apiSerializers.all.before, - apiSerializers.default.all - ]; + const stubsToCheck = [apiSerializers.all.before, apiSerializers.default.all]; - return shared.serializers.handle.output(response, apiConfig, apiSerializers, frame) + return shared.serializers.handle + .output(response, apiConfig, apiSerializers, frame) .then(() => { stubsToCheck.forEach((stub) => { sinon.assert.calledOnceWithExactly(stub, response, apiConfig, frame); }); // After has a different call signature... is this a intentional? - sinon.assert.calledOnceWithExactly(apiSerializers.all.after, apiConfig, frame); - - sinon.assert.callOrder(apiSerializers.all.before, apiSerializers.default.all, apiSerializers.all.after); + sinon.assert.calledOnceWithExactly( + apiSerializers.all.after, + apiConfig, + frame, + ); + + sinon.assert.callOrder( + apiSerializers.all.before, + apiSerializers.default.all, + apiSerializers.all.after, + ); sinon.assert.notCalled(apiSerializers.default.add); }); }); diff --git a/packages/api-framework/test/serializers/input/all.test.js b/packages/api-framework/test/serializers/input/all.test.js index 379848d95..37b7edcc9 100644 --- a/packages/api-framework/test/serializers/input/all.test.js +++ b/packages/api-framework/test/serializers/input/all.test.js @@ -1,22 +1,22 @@ -const assert = require('node:assert/strict'); -const shared = require('../../../'); +const assert = require("node:assert/strict"); +const shared = require("../../../"); -describe('serializers/input/all', function () { - describe('all', function () { - it('transforms into model readable format', function () { +describe("serializers/input/all", function () { + describe("all", function () { + it("transforms into model readable format", function () { const apiConfig = {}; const frame = { original: { - include: 'tags', - fields: 'id,status', - formats: 'html' + include: "tags", + fields: "id,status", + formats: "html", }, options: { - include: 'tags', - fields: 'id,status', - formats: 'html', - context: {} - } + include: "tags", + fields: "id,status", + formats: "html", + context: {}, + }, }; shared.serializers.input.all.all(apiConfig, frame); @@ -31,21 +31,21 @@ describe('serializers/input/all', function () { assert.ok(frame.options.columns); assert.ok(frame.options.withRelated); - assert.deepEqual(frame.options.withRelated, ['tags']); - assert.deepEqual(frame.options.columns, ['id', 'status', 'html']); - assert.deepEqual(frame.options.formats, ['html']); + assert.deepEqual(frame.options.withRelated, ["tags"]); + assert.deepEqual(frame.options.columns, ["id", "status", "html"]); + assert.deepEqual(frame.options.formats, ["html"]); }); - describe('extra allowed internal options', function () { - it('internal access', function () { + describe("extra allowed internal options", function () { + it("internal access", function () { const frame = { options: { context: { - internal: true + internal: true, }, transacting: true, - forUpdate: true - } + forUpdate: true, + }, }; const apiConfig = {}; @@ -57,15 +57,15 @@ describe('serializers/input/all', function () { assert.ok(frame.options.context); }); - it('no internal access', function () { + it("no internal access", function () { const frame = { options: { context: { - user: true + user: true, }, transacting: true, - forUpdate: true - } + forUpdate: true, + }, }; const apiConfig = {}; diff --git a/packages/api-framework/test/util/options.test.js b/packages/api-framework/test/util/options.test.js index 2dbb4bcd7..4447c8184 100644 --- a/packages/api-framework/test/util/options.test.js +++ b/packages/api-framework/test/util/options.test.js @@ -1,30 +1,33 @@ -const assert = require('node:assert/strict'); -const optionsUtil = require('../../lib/utils/options'); +const assert = require("node:assert/strict"); +const optionsUtil = require("../../lib/utils/options"); -describe('util/options', function () { - it('returns an array with empty string when no parameters are passed', function () { - assert.deepEqual(optionsUtil.trimAndLowerCase(), ['']); +describe("util/options", function () { + it("returns an array with empty string when no parameters are passed", function () { + assert.deepEqual(optionsUtil.trimAndLowerCase(), [""]); }); - it('returns single item array', function () { - assert.deepEqual(optionsUtil.trimAndLowerCase('butter'), ['butter']); + it("returns single item array", function () { + assert.deepEqual(optionsUtil.trimAndLowerCase("butter"), ["butter"]); }); - it('returns multiple items in array', function () { - assert.deepEqual(optionsUtil.trimAndLowerCase('peanut, butter'), ['peanut', 'butter']); + it("returns multiple items in array", function () { + assert.deepEqual(optionsUtil.trimAndLowerCase("peanut, butter"), ["peanut", "butter"]); }); - it('lowercases and trims items in the string', function () { - assert.deepEqual(optionsUtil.trimAndLowerCase(' PeanUt, buTTer '), ['peanut', 'butter']); + it("lowercases and trims items in the string", function () { + assert.deepEqual(optionsUtil.trimAndLowerCase(" PeanUt, buTTer "), ["peanut", "butter"]); }); - it('accepts parameters in form of an array', function () { - assert.deepEqual(optionsUtil.trimAndLowerCase([' PeanUt', ' buTTer ']), ['peanut', 'butter']); + it("accepts parameters in form of an array", function () { + assert.deepEqual(optionsUtil.trimAndLowerCase([" PeanUt", " buTTer "]), [ + "peanut", + "butter", + ]); }); - it('throws error for invalid object input', function () { - assert.throws(() => optionsUtil.trimAndLowerCase({name: 'peanut'}), { - message: 'Params must be a string or array' + it("throws error for invalid object input", function () { + assert.throws(() => optionsUtil.trimAndLowerCase({ name: "peanut" }), { + message: "Params must be a string or array", }); }); }); diff --git a/packages/api-framework/test/validators/handle.test.js b/packages/api-framework/test/validators/handle.test.js index 86421f12a..4664b05b2 100644 --- a/packages/api-framework/test/validators/handle.test.js +++ b/packages/api-framework/test/validators/handle.test.js @@ -1,61 +1,65 @@ -const errors = require('@tryghost/errors'); -const assert = require('node:assert/strict'); -const sinon = require('sinon'); -const shared = require('../../'); +const errors = require("@tryghost/errors"); +const assert = require("node:assert/strict"); +const sinon = require("sinon"); +const shared = require("../../"); -describe('validators/handle', function () { +describe("validators/handle", function () { afterEach(function () { sinon.restore(); }); - describe('input', function () { - it('no api config passed', function () { - return shared.validators.handle.input() + describe("input", function () { + it("no api config passed", function () { + return shared.validators.handle + .input() .then(Promise.reject) .catch((err) => { assert.equal(err instanceof errors.IncorrectUsageError, true); }); }); - it('no api validators passed', function () { - return shared.validators.handle.input({}) + it("no api validators passed", function () { + return shared.validators.handle + .input({}) .then(Promise.reject) .catch((err) => { assert.equal(err instanceof errors.IncorrectUsageError, true); }); }); - it('no api config passed when validators exist', function () { - return shared.validators.handle.input(undefined, {}, {}) + it("no api config passed when validators exist", function () { + return shared.validators.handle + .input(undefined, {}, {}) .then(Promise.reject) .catch((err) => { assert.equal(err instanceof errors.IncorrectUsageError, true); }); }); - it('ensure validators are called', function () { + it("ensure validators are called", function () { const getStub = sinon.stub(); const addStub = sinon.stub(); - sinon.stub(shared.validators.input.all, 'all').get(() => { + sinon.stub(shared.validators.input.all, "all").get(() => { return getStub; }); - sinon.stub(shared.validators.input.all, 'add').get(() => { + sinon.stub(shared.validators.input.all, "add").get(() => { return addStub; }); const apiValidators = { all: { - add: sinon.stub().resolves() + add: sinon.stub().resolves(), }, posts: { - add: sinon.stub().resolves() + add: sinon.stub().resolves(), }, users: { - add: sinon.stub().resolves() - } + add: sinon.stub().resolves(), + }, }; - return shared.validators.handle.input({docName: 'posts', method: 'add'}, apiValidators, {context: {}}) + return shared.validators.handle + .input({ docName: "posts", method: "add" }, apiValidators, { context: {} }) .then(() => { assert.equal(getStub.calledOnce, true); assert.equal(addStub.calledOnce, true); @@ -65,14 +69,15 @@ describe('validators/handle', function () { }); }); - it('calls docName all validator when provided', function () { + it("calls docName all validator when provided", function () { const apiValidators = { posts: { - all: sinon.stub().resolves() - } + all: sinon.stub().resolves(), + }, }; - return shared.validators.handle.input({docName: 'posts', method: 'browse'}, apiValidators, {}) + return shared.validators.handle + .input({ docName: "posts", method: "browse" }, apiValidators, {}) .then(() => { assert.equal(apiValidators.posts.all.calledOnce, true); }); diff --git a/packages/api-framework/test/validators/input/all.test.js b/packages/api-framework/test/validators/input/all.test.js index 66febed0e..a715e4806 100644 --- a/packages/api-framework/test/validators/input/all.test.js +++ b/packages/api-framework/test/validators/input/all.test.js @@ -1,250 +1,254 @@ -const errors = require('@tryghost/errors'); -const assert = require('node:assert/strict'); -const sinon = require('sinon'); -const shared = require('../../../'); +const errors = require("@tryghost/errors"); +const assert = require("node:assert/strict"); +const sinon = require("sinon"); +const shared = require("../../../"); -describe('validators/input/all', function () { +describe("validators/input/all", function () { afterEach(function () { sinon.restore(); }); - describe('all', function () { - it('default', function () { + describe("all", function () { + it("default", function () { const frame = { options: { context: {}, - slug: 'slug', - include: 'tags,authors', - page: 2 - } + slug: "slug", + include: "tags,authors", + page: 2, + }, }; const apiConfig = { options: { include: { - values: ['tags', 'authors'], - required: true - } - } - }; - - return shared.validators.input.all.all(apiConfig, frame) - .then(() => { - assert.ok(frame.options.page); - assert.ok(frame.options.slug); - assert.ok(frame.options.include); - assert.ok(frame.options.context); - }); + values: ["tags", "authors"], + required: true, + }, + }, + }; + + return shared.validators.input.all.all(apiConfig, frame).then(() => { + assert.ok(frame.options.page); + assert.ok(frame.options.slug); + assert.ok(frame.options.include); + assert.ok(frame.options.context); + }); }); - it('should run global validations on an type that has validation defined', function () { + it("should run global validations on an type that has validation defined", function () { const frame = { options: { - slug: 'not a valid slug %%%%% http://' - } + slug: "not a valid slug %%%%% http://", + }, }; const apiConfig = { options: { slug: { - required: true - } - } + required: true, + }, + }, }; - return shared.validators.input.all.all(apiConfig, frame) - .then(() => { - throw new Error('Should not resolve'); - }, (err) => { + return shared.validators.input.all.all(apiConfig, frame).then( + () => { + throw new Error("Should not resolve"); + }, + (err) => { assert.ok(err); - }); + }, + ); }); - it('allows empty values', function () { + it("allows empty values", function () { const frame = { options: { context: {}, - formats: '' - } + formats: "", + }, }; const apiConfig = { options: { - formats: ['format1'] - } + formats: ["format1"], + }, }; return shared.validators.input.all.all(apiConfig, frame); }); - it('supports include being an array', function () { + it("supports include being an array", function () { const frame = { options: { context: {}, - slug: 'slug', - include: ['tags', 'authors'], - page: 2 - } + slug: "slug", + include: ["tags", "authors"], + page: 2, + }, }; const apiConfig = { options: { include: { - values: ['tags', 'authors'], - required: true - } - } - }; - - return shared.validators.input.all.all(apiConfig, frame) - .then(() => { - assert.ok(frame.options.page); - assert.ok(frame.options.slug); - assert.ok(frame.options.include); - assert.ok(frame.options.context); - }); + values: ["tags", "authors"], + required: true, + }, + }, + }; + + return shared.validators.input.all.all(apiConfig, frame).then(() => { + assert.ok(frame.options.page); + assert.ok(frame.options.slug); + assert.ok(frame.options.include); + assert.ok(frame.options.context); + }); }); - it('default include array notation', function () { + it("default include array notation", function () { const frame = { options: { context: {}, - slug: 'slug', - include: 'tags,authors', - page: 2 - } + slug: "slug", + include: "tags,authors", + page: 2, + }, }; const apiConfig = { options: { - include: ['tags', 'authors'] - } + include: ["tags", "authors"], + }, }; - return shared.validators.input.all.all(apiConfig, frame) - .then(() => { - assert.ok(frame.options.page); - assert.ok(frame.options.slug); - assert.ok(frame.options.include); - assert.ok(frame.options.context); - }); + return shared.validators.input.all.all(apiConfig, frame).then(() => { + assert.ok(frame.options.page); + assert.ok(frame.options.slug); + assert.ok(frame.options.include); + assert.ok(frame.options.context); + }); }); - it('does not fail', function () { + it("does not fail", function () { const frame = { options: { context: {}, - include: 'tags,authors' - } + include: "tags,authors", + }, }; const apiConfig = { options: { include: { - values: ['tags'] - } - } + values: ["tags"], + }, + }, }; - return shared.validators.input.all.all(apiConfig, frame) + return shared.validators.input.all + .all(apiConfig, frame) .then(Promise.reject.bind(Promise)) .catch((err) => { assert.equal(err, undefined); }); }); - it('does not fail include array notation', function () { + it("does not fail include array notation", function () { const frame = { options: { context: {}, - include: 'tags,authors' - } + include: "tags,authors", + }, }; const apiConfig = { options: { - include: ['tags'] - } + include: ["tags"], + }, }; - return shared.validators.input.all.all(apiConfig, frame) + return shared.validators.input.all + .all(apiConfig, frame) .then(Promise.reject.bind(Promise)) .catch((err) => { assert.equal(err, undefined); }); }); - it('fails', function () { + it("fails", function () { const frame = { options: { - context: {} - } + context: {}, + }, }; const apiConfig = { options: { include: { - required: true - } - } + required: true, + }, + }, }; - return shared.validators.input.all.all(apiConfig, frame) + return shared.validators.input.all + .all(apiConfig, frame) .then(Promise.reject) .catch((err) => { assert.ok(err); }); }); - it('invalid fields', function () { + it("invalid fields", function () { const frame = { options: { context: {}, - id: 'invalid' - } + id: "invalid", + }, }; const apiConfig = {}; - return shared.validators.input.all.all(apiConfig, frame) + return shared.validators.input.all + .all(apiConfig, frame) .then(Promise.reject) .catch((err) => { assert.ok(err); }); }); - it('fails on invalid allowed values for non-include fields', function () { + it("fails on invalid allowed values for non-include fields", function () { const frame = { options: { context: {}, - formats: 'mobiledoc' - } + formats: "mobiledoc", + }, }; const apiConfig = { options: { - formats: ['html'] - } + formats: ["html"], + }, }; - return shared.validators.input.all.all(apiConfig, frame) + return shared.validators.input.all + .all(apiConfig, frame) .then(Promise.reject) .catch((err) => { assert.ok(err); - assert.equal(err.message, 'Validation (AllowedValues) failed for formats'); + assert.equal(err.message, "Validation (AllowedValues) failed for formats"); }); }); }); - describe('browse', function () { - it('default', function () { + describe("browse", function () { + it("default", function () { const frame = { options: { - context: {} + context: {}, }, data: { - status: 'aus' - } + status: "aus", + }, }; const apiConfig = {}; @@ -254,19 +258,20 @@ describe('validators/input/all', function () { assert.ok(frame.data.status); }); - it('fails', function () { + it("fails", function () { const frame = { options: { - context: {} + context: {}, }, data: { - id: 'no-id' - } + id: "no-id", + }, }; const apiConfig = {}; - return shared.validators.input.all.browse(apiConfig, frame) + return shared.validators.input.all + .browse(apiConfig, frame) .then(Promise.reject) .catch((err) => { assert.ok(err); @@ -274,14 +279,14 @@ describe('validators/input/all', function () { }); }); - describe('read', function () { - it('default', function () { - sinon.stub(shared.validators.input.all, 'browse'); + describe("read", function () { + it("default", function () { + sinon.stub(shared.validators.input.all, "browse"); const frame = { options: { - context: {} - } + context: {}, + }, }; const apiConfig = {}; @@ -291,60 +296,65 @@ describe('validators/input/all', function () { }); }); - describe('add', function () { - it('fails', function () { + describe("add", function () { + it("fails", function () { const frame = { - data: {} + data: {}, }; const apiConfig = { - docName: 'docName' + docName: "docName", }; - return shared.validators.input.all.add(apiConfig, frame) + return shared.validators.input.all + .add(apiConfig, frame) .then(Promise.reject) .catch((err) => { assert.ok(err); }); }); - it('fails with docName', function () { + it("fails with docName", function () { const frame = { data: { - docName: true - } + docName: true, + }, }; const apiConfig = { - docName: 'docName' + docName: "docName", }; - return shared.validators.input.all.add(apiConfig, frame) + return shared.validators.input.all + .add(apiConfig, frame) .then(Promise.reject) .catch((err) => { assert.ok(err); }); }); - it('fails for required field', function () { + it("fails for required field", function () { const frame = { data: { - docName: [{ - a: 'b' - }] - } + docName: [ + { + a: "b", + }, + ], + }, }; const apiConfig = { - docName: 'docName', + docName: "docName", data: { b: { - required: true - } - } + required: true, + }, + }, }; - return shared.validators.input.all.add(apiConfig, frame) + return shared.validators.input.all + .add(apiConfig, frame) .then(Promise.reject) .catch((err) => { assert.ok(err); @@ -352,26 +362,29 @@ describe('validators/input/all', function () { }); }); - it('fails for invalid field', function () { + it("fails for invalid field", function () { const frame = { data: { - docName: [{ - a: 'b', - b: null - }] - } + docName: [ + { + a: "b", + b: null, + }, + ], + }, }; const apiConfig = { - docName: 'docName', + docName: "docName", data: { b: { - required: true - } - } + required: true, + }, + }, }; - return shared.validators.input.all.add(apiConfig, frame) + return shared.validators.input.all + .add(apiConfig, frame) .then(Promise.reject) .catch((err) => { assert.ok(err); @@ -379,17 +392,19 @@ describe('validators/input/all', function () { }); }); - it('success', function () { + it("success", function () { const frame = { data: { - docName: [{ - a: 'b' - }] - } + docName: [ + { + a: "b", + }, + ], + }, }; const apiConfig = { - docName: 'docName' + docName: "docName", }; const result = shared.validators.input.all.add(apiConfig, frame); @@ -397,98 +412,109 @@ describe('validators/input/all', function () { }); }); - describe('edit', function () { - it('id mismatch', function () { + describe("edit", function () { + it("id mismatch", function () { const apiConfig = { - docName: 'users' + docName: "users", }; const frame = { options: { - id: 'zwei' + id: "zwei", }, data: { posts: [ { - id: 'eins' - } - ] - } + id: "eins", + }, + ], + }, }; - return shared.validators.input.all.edit(apiConfig, frame) + return shared.validators.input.all + .edit(apiConfig, frame) .then(Promise.reject) .catch((err) => { assert.equal(err instanceof errors.BadRequestError, true); }); }); - it('returns add promise result when add fails', function () { - sinon.stub(shared.validators.input.all, 'add').returns(Promise.reject(new Error('add-failed'))); - return shared.validators.input.all.edit({}, {}) + it("returns add promise result when add fails", function () { + sinon + .stub(shared.validators.input.all, "add") + .returns(Promise.reject(new Error("add-failed"))); + return shared.validators.input.all + .edit({}, {}) .then(Promise.reject) .catch((err) => { - assert.equal(err.message, 'add-failed'); + assert.equal(err.message, "add-failed"); }); }); - it('checks id mismatch after successful add for non posts/tags', function () { - sinon.stub(shared.validators.input.all, 'add').returns(undefined); - - return shared.validators.input.all.edit({ - docName: 'users' - }, { - options: { - id: 'id-1' - }, - data: { - users: [{id: 'id-2'}] - } - }) + it("checks id mismatch after successful add for non posts/tags", function () { + sinon.stub(shared.validators.input.all, "add").returns(undefined); + + return shared.validators.input.all + .edit( + { + docName: "users", + }, + { + options: { + id: "id-1", + }, + data: { + users: [{ id: "id-2" }], + }, + }, + ) .then(Promise.reject) .catch((err) => { assert.equal(err instanceof errors.BadRequestError, true); - assert.equal(err.message, 'Invalid id provided.'); + assert.equal(err.message, "Invalid id provided."); }); }); - it('does not check id mismatch for posts/tags', function () { - sinon.stub(shared.validators.input.all, 'add').returns(undefined); - const result = shared.validators.input.all.edit({ - docName: 'posts' - }, { - options: {id: 'id-1'}, - data: { - posts: [{id: 'id-2'}] - } - }); + it("does not check id mismatch for posts/tags", function () { + sinon.stub(shared.validators.input.all, "add").returns(undefined); + const result = shared.validators.input.all.edit( + { + docName: "posts", + }, + { + options: { id: "id-1" }, + data: { + posts: [{ id: "id-2" }], + }, + }, + ); assert.equal(result, undefined); }); }); - describe('delegated methods', function () { - it('changePassword delegates to add', function () { - sinon.stub(shared.validators.input.all, 'add').returns('add-result'); + describe("delegated methods", function () { + it("changePassword delegates to add", function () { + sinon.stub(shared.validators.input.all, "add").returns("add-result"); const result = shared.validators.input.all.changePassword({}, {}); - assert.equal(result, 'add-result'); + assert.equal(result, "add-result"); }); - it('resetPassword delegates to add', function () { - sinon.stub(shared.validators.input.all, 'add').returns('add-result'); + it("resetPassword delegates to add", function () { + sinon.stub(shared.validators.input.all, "add").returns("add-result"); const result = shared.validators.input.all.resetPassword({}, {}); - assert.equal(result, 'add-result'); + assert.equal(result, "add-result"); }); - it('setup delegates to add', function () { - sinon.stub(shared.validators.input.all, 'add').returns('add-result'); + it("setup delegates to add", function () { + sinon.stub(shared.validators.input.all, "add").returns("add-result"); const result = shared.validators.input.all.setup({}, {}); - assert.equal(result, 'add-result'); + assert.equal(result, "add-result"); }); - it('publish delegates to browse', function () { - sinon.stub(shared.validators.input.all, 'browse').returns('browse-result'); + it("publish delegates to browse", function () { + sinon.stub(shared.validators.input.all, "browse").returns("browse-result"); const result = shared.validators.input.all.publish({}, {}); - assert.equal(result, 'browse-result'); + assert.equal(result, "browse-result"); }); }); }); diff --git a/packages/bookshelf-collision/README.md b/packages/bookshelf-collision/README.md index 918097986..a24b7e0c4 100644 --- a/packages/bookshelf-collision/README.md +++ b/packages/bookshelf-collision/README.md @@ -8,36 +8,30 @@ or `yarn add @tryghost/bookshelf-collision` - ## Purpose Bookshelf plugin that detects stale updates and throws an update-collision error when client and server versions diverge. ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests +# Copyright & License - - -# Copyright & License - -Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/bookshelf-collision/index.js b/packages/bookshelf-collision/index.js index 9de9d59ff..de8a16e2d 100644 --- a/packages/bookshelf-collision/index.js +++ b/packages/bookshelf-collision/index.js @@ -1 +1 @@ -module.exports = require('./lib/bookshelf-collision'); +module.exports = require("./lib/bookshelf-collision"); diff --git a/packages/bookshelf-collision/lib/bookshelf-collision.js b/packages/bookshelf-collision/lib/bookshelf-collision.js index d0a01d9fe..95aee8578 100644 --- a/packages/bookshelf-collision/lib/bookshelf-collision.js +++ b/packages/bookshelf-collision/lib/bookshelf-collision.js @@ -1,6 +1,6 @@ -const moment = require('moment-timezone'); -const _ = require('lodash'); -const errors = require('@tryghost/errors'); +const moment = require("moment-timezone"); +const _ = require("lodash"); +const errors = require("@tryghost/errors"); /** * @param {import('bookshelf')} Bookshelf @@ -30,9 +30,11 @@ module.exports = function (Bookshelf) { const self = this; // CASE: only enabled for posts table - if (this.tableName !== 'posts' || + if ( + this.tableName !== "posts" || !self.serverData || - ((options.method !== 'update' && options.method !== 'patch') || !options.method) + (options.method !== "update" && options.method !== "patch") || + !options.method ) { return parentSync; } @@ -49,11 +51,19 @@ module.exports = function (Bookshelf) { parentSync.update = async function update() { const response = await originalUpdateSync.apply(this, arguments); const changed = _.omit(self._changed, [ - 'created_at', 'updated_at', 'author_id', 'id', - 'published_by', 'updated_by', 'html', 'plaintext' + "created_at", + "updated_at", + "author_id", + "id", + "published_by", + "updated_by", + "html", + "plaintext", ]); - const clientUpdatedAt = moment(self.clientData.updated_at || self.serverData.updated_at || new Date()); + const clientUpdatedAt = moment( + self.clientData.updated_at || self.serverData.updated_at || new Date(), + ); const serverUpdatedAt = moment(self.serverData.updated_at || clientUpdatedAt); const changedFields = Object.keys(changed); @@ -62,14 +72,14 @@ module.exports = function (Bookshelf) { if (clientUpdatedAt.diff(serverUpdatedAt) !== 0) { // @NOTE: This will rollback the update. We cannot know if relations were updated before doing the update. throw new errors.UpdateCollisionError({ - message: 'Saving failed! Someone else is editing this post.', - code: 'UPDATE_COLLISION', - level: 'critical', + message: "Saving failed! Someone else is editing this post.", + code: "UPDATE_COLLISION", + level: "critical", errorDetails: { changedFields, clientUpdatedAt: self.clientData.updated_at, - serverUpdatedAt: self.serverData.updated_at - } + serverUpdatedAt: self.serverData.updated_at, + }, }); } } @@ -91,7 +101,7 @@ module.exports = function (Bookshelf) { this.serverData = _.cloneDeep(this.attributes); return ParentModel.prototype.save.apply(this, arguments); - } + }, }); Bookshelf.Model = Model; diff --git a/packages/bookshelf-collision/package.json b/packages/bookshelf-collision/package.json index 3f34fb02e..3336f6838 100644 --- a/packages/bookshelf-collision/package.json +++ b/packages/bookshelf-collision/package.json @@ -1,34 +1,34 @@ { "name": "@tryghost/bookshelf-collision", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/bookshelf-collision" }, - "author": "Ghost Foundation", - "license": "MIT", - "main": "index.js", - "scripts": { - "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" - }, "files": [ "index.js", "lib" ], + "main": "index.js", "publishConfig": { "access": "public" }, - "devDependencies": { - "sinon": "21.0.3" + "scripts": { + "dev": "echo \"Implement me!\"", + "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "dependencies": { "@tryghost/errors": "^3.0.3", "lodash": "4.17.23", "moment-timezone": "^0.5.33" + }, + "devDependencies": { + "sinon": "21.0.3" } } diff --git a/packages/bookshelf-collision/test/.eslintrc.js b/packages/bookshelf-collision/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/bookshelf-collision/test/.eslintrc.js +++ b/packages/bookshelf-collision/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/bookshelf-collision/test/bookshelf-collision.test.js b/packages/bookshelf-collision/test/bookshelf-collision.test.js index 790afc6a6..8a9c6faec 100644 --- a/packages/bookshelf-collision/test/bookshelf-collision.test.js +++ b/packages/bookshelf-collision/test/bookshelf-collision.test.js @@ -1,9 +1,9 @@ -const assert = require('node:assert/strict'); -const sinon = require('sinon'); -const errors = require('@tryghost/errors'); -const installPlugin = require('..'); +const assert = require("node:assert/strict"); +const sinon = require("sinon"); +const errors = require("@tryghost/errors"); +const installPlugin = require(".."); -describe('@tryghost/bookshelf-collision', function () { +describe("@tryghost/bookshelf-collision", function () { let Bookshelf; let ParentModel; let parentSync; @@ -11,9 +11,9 @@ describe('@tryghost/bookshelf-collision', function () { let parentSave; beforeEach(function () { - parentUpdate = sinon.stub().resolves('UPDATED'); - parentSync = {update: parentUpdate}; - parentSave = sinon.stub().resolves('SAVED'); + parentUpdate = sinon.stub().resolves("UPDATED"); + parentSync = { update: parentUpdate }; + parentSave = sinon.stub().resolves("SAVED"); ParentModel = function BaseModel() { this.attributes = {}; @@ -34,7 +34,7 @@ describe('@tryghost/bookshelf-collision', function () { return Child; }; - Bookshelf = {Model: ParentModel}; + Bookshelf = { Model: ParentModel }; installPlugin(Bookshelf); }); @@ -42,155 +42,158 @@ describe('@tryghost/bookshelf-collision', function () { sinon.restore(); }); - it('exports plugin from index', function () { - assert.equal(typeof require('../index'), 'function'); + it("exports plugin from index", function () { + assert.equal(typeof require("../index"), "function"); }); - it('replaces Bookshelf.Model with extended model', function () { + it("replaces Bookshelf.Model with extended model", function () { assert.notEqual(Bookshelf.Model, ParentModel); }); - it('save stores cloned client/server data and calls parent save', async function () { + it("save stores cloned client/server data and calls parent save", async function () { const Model = Bookshelf.Model; const model = new Model(); - model.attributes = {id: 1, updated_at: '2024-01-01T00:00:00.000Z'}; - const clientPayload = {title: 'post'}; + model.attributes = { id: 1, updated_at: "2024-01-01T00:00:00.000Z" }; + const clientPayload = { title: "post" }; const result = await model.save(clientPayload); - assert.equal(result, 'SAVED'); + assert.equal(result, "SAVED"); assert.equal(parentSave.calledOnce, true); - assert.deepEqual(model.clientData, {title: 'post'}); - assert.deepEqual(model.serverData, {id: 1, updated_at: '2024-01-01T00:00:00.000Z'}); + assert.deepEqual(model.clientData, { title: "post" }); + assert.deepEqual(model.serverData, { id: 1, updated_at: "2024-01-01T00:00:00.000Z" }); assert.notEqual(model.clientData, clientPayload); assert.notEqual(model.serverData, model.attributes); }); - it('save defaults clientData to empty object when no data is passed', async function () { + it("save defaults clientData to empty object when no data is passed", async function () { const Model = Bookshelf.Model; const model = new Model(); - model.attributes = {id: 1}; + model.attributes = { id: 1 }; await model.save(); assert.deepEqual(model.clientData, {}); - assert.deepEqual(model.serverData, {id: 1}); + assert.deepEqual(model.serverData, { id: 1 }); }); - it('sync returns parent sync for non-post tables', function () { + it("sync returns parent sync for non-post tables", function () { const Model = Bookshelf.Model; const model = new Model(); - model.tableName = 'users'; - model.serverData = {updated_at: '2024-01-01T00:00:00.000Z'}; + model.tableName = "users"; + model.serverData = { updated_at: "2024-01-01T00:00:00.000Z" }; - const result = model.sync({method: 'update'}); + const result = model.sync({ method: "update" }); assert.equal(result, parentSync); assert.equal(result.update, parentUpdate); }); - it('sync returns parent sync when serverData is missing', function () { + it("sync returns parent sync when serverData is missing", function () { const Model = Bookshelf.Model; const model = new Model(); - model.tableName = 'posts'; + model.tableName = "posts"; - const result = model.sync({method: 'patch'}); + const result = model.sync({ method: "patch" }); assert.equal(result, parentSync); assert.equal(result.update, parentUpdate); }); - it('sync returns parent sync for unsupported methods', function () { + it("sync returns parent sync for unsupported methods", function () { const Model = Bookshelf.Model; const model = new Model(); - model.tableName = 'posts'; - model.serverData = {updated_at: '2024-01-01T00:00:00.000Z'}; + model.tableName = "posts"; + model.serverData = { updated_at: "2024-01-01T00:00:00.000Z" }; - const result = model.sync({method: 'insert'}); + const result = model.sync({ method: "insert" }); assert.equal(result, parentSync); assert.equal(result.update, parentUpdate); }); - it('sync wraps update for post updates/patches', function () { + it("sync wraps update for post updates/patches", function () { const Model = Bookshelf.Model; const model = new Model(); - model.tableName = 'posts'; - model.serverData = {updated_at: '2024-01-01T00:00:00.000Z'}; + model.tableName = "posts"; + model.serverData = { updated_at: "2024-01-01T00:00:00.000Z" }; - const updateSync = model.sync({method: 'patch'}); + const updateSync = model.sync({ method: "patch" }); assert.equal(updateSync, parentSync); assert.notEqual(updateSync.update, parentUpdate); }); - it('wrapped update returns response when only ignored fields changed', async function () { + it("wrapped update returns response when only ignored fields changed", async function () { const Model = Bookshelf.Model; const model = new Model(); - model.tableName = 'posts'; - model.serverData = {updated_at: '2024-01-01T00:00:00.000Z'}; - model.clientData = {updated_at: '2024-01-02T00:00:00.000Z'}; - model._changed = {updated_at: '2024-01-02T00:00:00.000Z', html: '

x

'}; + model.tableName = "posts"; + model.serverData = { updated_at: "2024-01-01T00:00:00.000Z" }; + model.clientData = { updated_at: "2024-01-02T00:00:00.000Z" }; + model._changed = { updated_at: "2024-01-02T00:00:00.000Z", html: "

x

" }; - const updateSync = model.sync({method: 'update'}); + const updateSync = model.sync({ method: "update" }); const result = await updateSync.update(); - assert.equal(result, 'UPDATED'); + assert.equal(result, "UPDATED"); assert.equal(parentUpdate.calledOnce, true); }); - it('wrapped update returns response when changed fields exist but timestamps match', async function () { + it("wrapped update returns response when changed fields exist but timestamps match", async function () { const Model = Bookshelf.Model; const model = new Model(); - model.tableName = 'posts'; - model.serverData = {updated_at: '2024-01-01T00:00:00.000Z'}; - model.clientData = {updated_at: '2024-01-01T00:00:00.000Z'}; - model._changed = {title: 'changed'}; + model.tableName = "posts"; + model.serverData = { updated_at: "2024-01-01T00:00:00.000Z" }; + model.clientData = { updated_at: "2024-01-01T00:00:00.000Z" }; + model._changed = { title: "changed" }; - const updateSync = model.sync({method: 'update'}); - const result = await updateSync.update('a1', 'a2'); + const updateSync = model.sync({ method: "update" }); + const result = await updateSync.update("a1", "a2"); - assert.equal(result, 'UPDATED'); - assert.equal(parentUpdate.calledOnceWithExactly('a1', 'a2'), true); + assert.equal(result, "UPDATED"); + assert.equal(parentUpdate.calledOnceWithExactly("a1", "a2"), true); }); - it('wrapped update throws UpdateCollisionError when timestamps differ and meaningful fields changed', async function () { + it("wrapped update throws UpdateCollisionError when timestamps differ and meaningful fields changed", async function () { const Model = Bookshelf.Model; const model = new Model(); - model.tableName = 'posts'; - model.serverData = {updated_at: '2024-01-01T00:00:00.000Z'}; - model.clientData = {updated_at: '2024-01-02T00:00:00.000Z'}; - model._changed = {title: 'new-title'}; - - const updateSync = model.sync({method: 'update'}); - - await assert.rejects(async function () { - await updateSync.update(); - }, function (err) { - assert.equal(err instanceof errors.UpdateCollisionError, true); - assert.equal(err.code, 'UPDATE_COLLISION'); - assert.deepEqual(err.errorDetails, { - changedFields: ['title'], - clientUpdatedAt: '2024-01-02T00:00:00.000Z', - serverUpdatedAt: '2024-01-01T00:00:00.000Z' - }); - return true; - }); + model.tableName = "posts"; + model.serverData = { updated_at: "2024-01-01T00:00:00.000Z" }; + model.clientData = { updated_at: "2024-01-02T00:00:00.000Z" }; + model._changed = { title: "new-title" }; + + const updateSync = model.sync({ method: "update" }); + + await assert.rejects( + async function () { + await updateSync.update(); + }, + function (err) { + assert.equal(err instanceof errors.UpdateCollisionError, true); + assert.equal(err.code, "UPDATE_COLLISION"); + assert.deepEqual(err.errorDetails, { + changedFields: ["title"], + clientUpdatedAt: "2024-01-02T00:00:00.000Z", + serverUpdatedAt: "2024-01-01T00:00:00.000Z", + }); + return true; + }, + ); assert.equal(parentUpdate.calledOnce, true); }); - it('falls back to current date when no timestamps are present and no fields changed', async function () { + it("falls back to current date when no timestamps are present and no fields changed", async function () { const Model = Bookshelf.Model; const model = new Model(); - model.tableName = 'posts'; + model.tableName = "posts"; model.serverData = {}; model.clientData = {}; model._changed = {}; - const updateSync = model.sync({method: 'update'}); + const updateSync = model.sync({ method: "update" }); const result = await updateSync.update(); - assert.equal(result, 'UPDATED'); + assert.equal(result, "UPDATED"); }); }); diff --git a/packages/bookshelf-custom-query/README.md b/packages/bookshelf-custom-query/README.md index 8d0ac92d2..5e0208494 100644 --- a/packages/bookshelf-custom-query/README.md +++ b/packages/bookshelf-custom-query/README.md @@ -8,36 +8,30 @@ or `yarn add @tryghost/bookshelf-custom-query` - ## Purpose Bookshelf plugin that adds reusable `customQuery` hooks to models for centralized query-builder customization. ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests +# Copyright & License - - -# Copyright & License - -Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/bookshelf-custom-query/index.js b/packages/bookshelf-custom-query/index.js index eb6011607..3dcf3f3d5 100644 --- a/packages/bookshelf-custom-query/index.js +++ b/packages/bookshelf-custom-query/index.js @@ -1 +1 @@ -module.exports = require('./lib/bookshelf-custom-query'); +module.exports = require("./lib/bookshelf-custom-query"); diff --git a/packages/bookshelf-custom-query/lib/bookshelf-custom-query.js b/packages/bookshelf-custom-query/lib/bookshelf-custom-query.js index ec373b861..d30ef3930 100644 --- a/packages/bookshelf-custom-query/lib/bookshelf-custom-query.js +++ b/packages/bookshelf-custom-query/lib/bookshelf-custom-query.js @@ -10,7 +10,7 @@ const customQueryPlug = function customQueryPlug(Bookshelf) { this.query((qb) => { this.customQuery(qb, options); }); - } + }, }); Bookshelf.Model = Model; diff --git a/packages/bookshelf-custom-query/package.json b/packages/bookshelf-custom-query/package.json index 059404c76..82894c59e 100644 --- a/packages/bookshelf-custom-query/package.json +++ b/packages/bookshelf-custom-query/package.json @@ -1,14 +1,21 @@ { "name": "@tryghost/bookshelf-custom-query", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/bookshelf-custom-query" }, - "author": "Ghost Foundation", - "license": "MIT", + "files": [ + "index.js", + "lib" + ], "main": "index.js", + "publishConfig": { + "access": "public" + }, "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", @@ -16,13 +23,6 @@ "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, - "files": [ - "index.js", - "lib" - ], - "publishConfig": { - "access": "public" - }, "devDependencies": { "sinon": "21.0.3" } diff --git a/packages/bookshelf-custom-query/test/.eslintrc.js b/packages/bookshelf-custom-query/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/bookshelf-custom-query/test/.eslintrc.js +++ b/packages/bookshelf-custom-query/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/bookshelf-custom-query/test/bookshelf-custom-query.test.js b/packages/bookshelf-custom-query/test/bookshelf-custom-query.test.js index 048c88552..683b4869b 100644 --- a/packages/bookshelf-custom-query/test/bookshelf-custom-query.test.js +++ b/packages/bookshelf-custom-query/test/bookshelf-custom-query.test.js @@ -1,8 +1,8 @@ -const assert = require('node:assert/strict'); -const sinon = require('sinon'); -const installPlugin = require('..'); +const assert = require("node:assert/strict"); +const sinon = require("sinon"); +const installPlugin = require(".."); -describe('@tryghost/bookshelf-custom-query', function () { +describe("@tryghost/bookshelf-custom-query", function () { let Bookshelf; let ParentModel; @@ -17,7 +17,7 @@ describe('@tryghost/bookshelf-custom-query', function () { return Child; }; - Bookshelf = {Model: ParentModel}; + Bookshelf = { Model: ParentModel }; installPlugin(Bookshelf); }); @@ -25,27 +25,27 @@ describe('@tryghost/bookshelf-custom-query', function () { sinon.restore(); }); - it('exports plugin from index', function () { - assert.equal(typeof require('../index'), 'function'); + it("exports plugin from index", function () { + assert.equal(typeof require("../index"), "function"); }); - it('replaces Bookshelf.Model with extended model', function () { + it("replaces Bookshelf.Model with extended model", function () { assert.notEqual(Bookshelf.Model, ParentModel); }); - it('provides a default customQuery function', function () { + it("provides a default customQuery function", function () { const model = new Bookshelf.Model(); - assert.equal(typeof model.customQuery, 'function'); + assert.equal(typeof model.customQuery, "function"); assert.equal(model.customQuery(), undefined); }); - it('applyCustomQuery calls query and forwards qb/options to customQuery', function () { + it("applyCustomQuery calls query and forwards qb/options to customQuery", function () { const model = new Bookshelf.Model(); - const qb = {where: sinon.stub()}; - const options = {filter: 'status:published'}; + const qb = { where: sinon.stub() }; + const options = { filter: "status:published" }; model.customQuery = sinon.stub(); - model.query = sinon.stub().callsFake(fn => fn(qb)); + model.query = sinon.stub().callsFake((fn) => fn(qb)); model.applyCustomQuery(options); diff --git a/packages/bookshelf-eager-load/README.md b/packages/bookshelf-eager-load/README.md index 282bbd7cc..368d3ec27 100644 --- a/packages/bookshelf-eager-load/README.md +++ b/packages/bookshelf-eager-load/README.md @@ -8,36 +8,30 @@ or `yarn add @tryghost/bookshelf-eager-load` - ## Purpose Bookshelf plugin that applies relation joins during fetch/fetchAll for eager-loading and relation-based ordering use cases. ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests +# Copyright & License - - -# Copyright & License - -Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/bookshelf-eager-load/index.js b/packages/bookshelf-eager-load/index.js index 5749892fd..d6b057a2b 100644 --- a/packages/bookshelf-eager-load/index.js +++ b/packages/bookshelf-eager-load/index.js @@ -1 +1 @@ -module.exports = require('./lib/bookshelf-eager-load'); +module.exports = require("./lib/bookshelf-eager-load"); diff --git a/packages/bookshelf-eager-load/lib/bookshelf-eager-load.js b/packages/bookshelf-eager-load/lib/bookshelf-eager-load.js index 8e6d62e98..b8b13877f 100644 --- a/packages/bookshelf-eager-load/lib/bookshelf-eager-load.js +++ b/packages/bookshelf-eager-load/lib/bookshelf-eager-load.js @@ -1,6 +1,6 @@ -const _ = require('lodash'); -const _debug = require('@tryghost/debug')._base; -const debug = _debug('ghost-query'); +const _ = require("lodash"); +const _debug = require("@tryghost/debug")._base; +const debug = _debug("ghost-query"); /** * Enchances knex query builder with a join to relation configured in @@ -9,7 +9,7 @@ const debug = _debug('ghost-query'); * @param {String[]} relationsToLoad relations to be included in joins */ function withEager(model, relationsToLoad) { - const tableName = _.result(model.constructor.prototype, 'tableName'); + const tableName = _.result(model.constructor.prototype, "tableName"); return function (qb) { if (!model.relationsMeta) { @@ -18,8 +18,11 @@ function withEager(model, relationsToLoad) { for (const [key, config] of Object.entries(model.relationsMeta)) { if (relationsToLoad.includes(key)) { - const innerQb = qb - .leftJoin(config.targetTableName, `${tableName}.id`, `${config.targetTableName}.${config.foreignKey}`); + const innerQb = qb.leftJoin( + config.targetTableName, + `${tableName}.id`, + `${config.targetTableName}.${config.foreignKey}`, + ); debug(`QUERY has posts: ${innerQb.toSQL().sql}`); } @@ -35,7 +38,11 @@ function load(options) { } if (this.eagerLoad) { - if (!options.columns && options.withRelated && _.intersection(this.eagerLoad, options.withRelated).length) { + if ( + !options.columns && + options.withRelated && + _.intersection(this.eagerLoad, options.withRelated).length + ) { this.query(withEager(this, this.eagerLoad)); } } @@ -58,8 +65,8 @@ module.exports = function eagerLoadPlugin(Bookshelf) { fetch: function () { load.apply(this, arguments); - if (_debug.enabled('ghost-query')) { - debug('QUERY', this.query().toQuery()); + if (_debug.enabled("ghost-query")) { + debug("QUERY", this.query().toQuery()); } return modelPrototype.fetch.apply(this, arguments); @@ -68,12 +75,12 @@ module.exports = function eagerLoadPlugin(Bookshelf) { fetchAll: function () { load.apply(this, arguments); - if (_debug.enabled('ghost-query')) { - debug('QUERY', this.query().toQuery()); + if (_debug.enabled("ghost-query")) { + debug("QUERY", this.query().toQuery()); } return modelPrototype.fetchAll.apply(this, arguments); - } + }, }); }; diff --git a/packages/bookshelf-eager-load/package.json b/packages/bookshelf-eager-load/package.json index d534b288b..02365fa53 100644 --- a/packages/bookshelf-eager-load/package.json +++ b/packages/bookshelf-eager-load/package.json @@ -1,33 +1,33 @@ { "name": "@tryghost/bookshelf-eager-load", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/bookshelf-eager-load" }, - "author": "Ghost Foundation", - "license": "MIT", - "main": "index.js", - "scripts": { - "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" - }, "files": [ "index.js", "lib" ], + "main": "index.js", "publishConfig": { "access": "public" }, - "devDependencies": { - "sinon": "21.0.3" + "scripts": { + "dev": "echo \"Implement me!\"", + "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "dependencies": { "@tryghost/debug": "^2.0.3", "lodash": "4.17.23" + }, + "devDependencies": { + "sinon": "21.0.3" } } diff --git a/packages/bookshelf-eager-load/test/.eslintrc.js b/packages/bookshelf-eager-load/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/bookshelf-eager-load/test/.eslintrc.js +++ b/packages/bookshelf-eager-load/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/bookshelf-eager-load/test/bookshelf-eager-load.test.js b/packages/bookshelf-eager-load/test/bookshelf-eager-load.test.js index 299a9580d..3750406b6 100644 --- a/packages/bookshelf-eager-load/test/bookshelf-eager-load.test.js +++ b/packages/bookshelf-eager-load/test/bookshelf-eager-load.test.js @@ -1,9 +1,9 @@ -const assert = require('node:assert/strict'); -const sinon = require('sinon'); -const debugBase = require('@tryghost/debug')._base; -const installPlugin = require('..'); +const assert = require("node:assert/strict"); +const sinon = require("sinon"); +const debugBase = require("@tryghost/debug")._base; +const installPlugin = require(".."); -describe('@tryghost/bookshelf-eager-load', function () { +describe("@tryghost/bookshelf-eager-load", function () { let Bookshelf; let ParentModel; let initStub; @@ -11,9 +11,9 @@ describe('@tryghost/bookshelf-eager-load', function () { let fetchAllStub; beforeEach(function () { - initStub = sinon.stub().returns('INIT'); - fetchStub = sinon.stub().returns('FETCH'); - fetchAllStub = sinon.stub().returns('FETCH_ALL'); + initStub = sinon.stub().returns("INIT"); + fetchStub = sinon.stub().returns("FETCH"); + fetchAllStub = sinon.stub().returns("FETCH_ALL"); ParentModel = function BaseModel() {}; ParentModel.prototype.initialize = initStub; @@ -29,7 +29,7 @@ describe('@tryghost/bookshelf-eager-load', function () { return Child; }; - Bookshelf = {Model: ParentModel}; + Bookshelf = { Model: ParentModel }; installPlugin(Bookshelf); }); @@ -37,168 +37,168 @@ describe('@tryghost/bookshelf-eager-load', function () { sinon.restore(); }); - it('exports plugin from index', function () { - assert.equal(typeof require('../index'), 'function'); + it("exports plugin from index", function () { + assert.equal(typeof require("../index"), "function"); }); - it('initialize calls parent initialize', function () { + it("initialize calls parent initialize", function () { const model = new Bookshelf.Model(); - const result = model.initialize('a1'); - assert.equal(result, 'INIT'); - assert.equal(initStub.calledOnceWithExactly('a1'), true); + const result = model.initialize("a1"); + assert.equal(result, "INIT"); + assert.equal(initStub.calledOnceWithExactly("a1"), true); }); - it('fetch calls parent fetch and skips load when options are missing', function () { - sinon.stub(debugBase, 'enabled').returns(false); + it("fetch calls parent fetch and skips load when options are missing", function () { + sinon.stub(debugBase, "enabled").returns(false); const model = new Bookshelf.Model(); - model.query = sinon.stub().returns({toQuery: sinon.stub().returns('SQL')}); + model.query = sinon.stub().returns({ toQuery: sinon.stub().returns("SQL") }); const result = model.fetch(); - assert.equal(result, 'FETCH'); + assert.equal(result, "FETCH"); assert.equal(fetchStub.calledOnce, true); }); - it('fetch loads eager join when configured and relation requested', function () { - sinon.stub(debugBase, 'enabled').returns(false); + it("fetch loads eager join when configured and relation requested", function () { + sinon.stub(debugBase, "enabled").returns(false); const model = new Bookshelf.Model(); - model.eagerLoad = ['authors']; + model.eagerLoad = ["authors"]; model.relationsMeta = { authors: { - targetTableName: 'users', - foreignKey: 'post_id' + targetTableName: "users", + foreignKey: "post_id", }, tags: { - targetTableName: 'tags', - foreignKey: 'post_id' - } + targetTableName: "tags", + foreignKey: "post_id", + }, }; - model.constructor = {prototype: {tableName: 'posts'}}; + model.constructor = { prototype: { tableName: "posts" } }; const leftJoin = sinon.stub().returns({ toSQL() { - return {sql: 'select *'}; - } + return { sql: "select *" }; + }, }); - const qb = {leftJoin}; + const qb = { leftJoin }; model.query = sinon.stub().callsFake((arg) => { - if (typeof arg === 'function') { + if (typeof arg === "function") { return arg(qb); } - return {toQuery: sinon.stub().returns('SQL')}; + return { toQuery: sinon.stub().returns("SQL") }; }); - const result = model.fetch({withRelated: ['authors', 'tags']}); + const result = model.fetch({ withRelated: ["authors", "tags"] }); - assert.equal(result, 'FETCH'); - assert.equal(leftJoin.calledOnceWithExactly('users', 'posts.id', 'users.post_id'), true); + assert.equal(result, "FETCH"); + assert.equal(leftJoin.calledOnceWithExactly("users", "posts.id", "users.post_id"), true); assert.equal(fetchStub.calledOnce, true); }); - it('fetch does not load eager join when columns are provided', function () { - sinon.stub(debugBase, 'enabled').returns(false); + it("fetch does not load eager join when columns are provided", function () { + sinon.stub(debugBase, "enabled").returns(false); const model = new Bookshelf.Model(); - model.eagerLoad = ['authors']; + model.eagerLoad = ["authors"]; model.relationsMeta = { authors: { - targetTableName: 'users', - foreignKey: 'post_id' - } + targetTableName: "users", + foreignKey: "post_id", + }, }; - model.constructor = {prototype: {tableName: 'posts'}}; + model.constructor = { prototype: { tableName: "posts" } }; const leftJoin = sinon.stub().returns({ toSQL() { - return {sql: 'select *'}; - } + return { sql: "select *" }; + }, }); - const qb = {leftJoin}; + const qb = { leftJoin }; model.query = sinon.stub().callsFake((arg) => { - if (typeof arg === 'function') { + if (typeof arg === "function") { return arg(qb); } - return {toQuery: sinon.stub().returns('SQL')}; + return { toQuery: sinon.stub().returns("SQL") }; }); - model.fetch({columns: ['id'], withRelated: ['authors']}); + model.fetch({ columns: ["id"], withRelated: ["authors"] }); assert.equal(leftJoin.called, false); }); - it('fetch does not load eager join when withRelated does not intersect eagerLoad', function () { - sinon.stub(debugBase, 'enabled').returns(false); + it("fetch does not load eager join when withRelated does not intersect eagerLoad", function () { + sinon.stub(debugBase, "enabled").returns(false); const model = new Bookshelf.Model(); - model.eagerLoad = ['authors']; + model.eagerLoad = ["authors"]; model.relationsMeta = { authors: { - targetTableName: 'users', - foreignKey: 'post_id' - } + targetTableName: "users", + foreignKey: "post_id", + }, }; - model.constructor = {prototype: {tableName: 'posts'}}; + model.constructor = { prototype: { tableName: "posts" } }; const leftJoin = sinon.stub().returns({ toSQL() { - return {sql: 'select *'}; - } + return { sql: "select *" }; + }, }); - const qb = {leftJoin}; + const qb = { leftJoin }; model.query = sinon.stub().callsFake((arg) => { - if (typeof arg === 'function') { + if (typeof arg === "function") { return arg(qb); } - return {toQuery: sinon.stub().returns('SQL')}; + return { toQuery: sinon.stub().returns("SQL") }; }); - model.fetch({withRelated: ['tags']}); + model.fetch({ withRelated: ["tags"] }); assert.equal(leftJoin.called, false); }); - it('fetch skips eager query when relationsMeta is missing', function () { - sinon.stub(debugBase, 'enabled').returns(false); + it("fetch skips eager query when relationsMeta is missing", function () { + sinon.stub(debugBase, "enabled").returns(false); const model = new Bookshelf.Model(); - model.eagerLoad = ['authors']; - model.constructor = {prototype: {tableName: 'posts'}}; + model.eagerLoad = ["authors"]; + model.constructor = { prototype: { tableName: "posts" } }; model.query = sinon.stub().callsFake((arg) => { - if (typeof arg === 'function') { + if (typeof arg === "function") { return arg({}); } - return {toQuery: sinon.stub().returns('SQL')}; + return { toQuery: sinon.stub().returns("SQL") }; }); - model.fetch({withRelated: ['authors']}); + model.fetch({ withRelated: ["authors"] }); assert.equal(fetchStub.calledOnce, true); }); - it('fetch logs query when debug is enabled', function () { - sinon.stub(debugBase, 'enabled').returns(true); + it("fetch logs query when debug is enabled", function () { + sinon.stub(debugBase, "enabled").returns(true); const model = new Bookshelf.Model(); model.query = sinon.stub().callsFake(() => { - return {toQuery: sinon.stub().returns('SELECT 1')}; + return { toQuery: sinon.stub().returns("SELECT 1") }; }); const result = model.fetch({}); - assert.equal(result, 'FETCH'); + assert.equal(result, "FETCH"); assert.equal(fetchStub.calledOnce, true); }); - it('fetchAll logs query when debug is enabled and calls parent fetchAll', function () { - sinon.stub(debugBase, 'enabled').returns(true); + it("fetchAll logs query when debug is enabled and calls parent fetchAll", function () { + sinon.stub(debugBase, "enabled").returns(true); const model = new Bookshelf.Model(); model.query = sinon.stub().callsFake(() => { - return {toQuery: sinon.stub().returns('SELECT 1')}; + return { toQuery: sinon.stub().returns("SELECT 1") }; }); const result = model.fetchAll({}); - assert.equal(result, 'FETCH_ALL'); + assert.equal(result, "FETCH_ALL"); assert.equal(fetchAllStub.calledOnce, true); }); }); diff --git a/packages/bookshelf-filter/README.md b/packages/bookshelf-filter/README.md index 4f5aee477..65ba60f37 100644 --- a/packages/bookshelf-filter/README.md +++ b/packages/bookshelf-filter/README.md @@ -8,36 +8,30 @@ or `yarn add @tryghost/bookshelf-filter` - ## Purpose Bookshelf plugin that parses and applies NQL filter expressions with support for defaults, overrides, and relation mappings. ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests +# Copyright & License - - -# Copyright & License - -Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/bookshelf-filter/index.js b/packages/bookshelf-filter/index.js index 150cb02a5..d6d17cdb6 100644 --- a/packages/bookshelf-filter/index.js +++ b/packages/bookshelf-filter/index.js @@ -1 +1 @@ -module.exports = require('./lib/bookshelf-filter'); +module.exports = require("./lib/bookshelf-filter"); diff --git a/packages/bookshelf-filter/lib/bookshelf-filter.js b/packages/bookshelf-filter/lib/bookshelf-filter.js index d9abcc3ca..34c970676 100644 --- a/packages/bookshelf-filter/lib/bookshelf-filter.js +++ b/packages/bookshelf-filter/lib/bookshelf-filter.js @@ -1,9 +1,9 @@ -const debug = require('@tryghost/debug')('models:plugins:filter'); -const errors = require('@tryghost/errors'); -const tpl = require('@tryghost/tpl'); +const debug = require("@tryghost/debug")("models:plugins:filter"); +const errors = require("@tryghost/errors"); +const tpl = require("@tryghost/tpl"); const messages = { - errorParsing: 'Error parsing filter' + errorParsing: "Error parsing filter", }; /** @@ -24,7 +24,7 @@ const filter = function filter(Bookshelf) { * instance. */ applyDefaultAndCustomFilters: function applyDefaultAndCustomFilters(options) { - const nql = require('@tryghost/nql'); + const nql = require("@tryghost/nql"); const expansions = []; @@ -40,10 +40,10 @@ const filter = function filter(Bookshelf) { let relations = this.filterRelations(options) || {}; let transformer = options.mongoTransformer; - debug('custom', custom); - debug('extra', extra); - debug('enforced', overrides); - debug('default', defaults); + debug("custom", custom); + debug("extra", extra); + debug("enforced", overrides); + debug("default", defaults); if (extra) { if (custom) { @@ -61,16 +61,16 @@ const filter = function filter(Bookshelf) { overrides: overrides, defaults: defaults, transformer: transformer, - cte: cte + cte: cte, }).querySQL(qb); }); } catch (err) { throw new errors.BadRequestError({ message: tpl(messages.errorParsing), - err + err, }); } - } + }, }); Bookshelf.Model = Model; diff --git a/packages/bookshelf-filter/package.json b/packages/bookshelf-filter/package.json index a61e43da0..733836c37 100644 --- a/packages/bookshelf-filter/package.json +++ b/packages/bookshelf-filter/package.json @@ -1,35 +1,35 @@ { "name": "@tryghost/bookshelf-filter", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/bookshelf-filter" }, - "author": "Ghost Foundation", - "license": "MIT", - "main": "index.js", - "scripts": { - "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" - }, "files": [ "index.js", "lib" ], + "main": "index.js", "publishConfig": { "access": "public" }, - "devDependencies": { - "sinon": "21.0.3" + "scripts": { + "dev": "echo \"Implement me!\"", + "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "dependencies": { "@tryghost/debug": "^2.0.3", "@tryghost/errors": "^3.0.3", "@tryghost/nql": "0.12.10", "@tryghost/tpl": "^2.0.3" + }, + "devDependencies": { + "sinon": "21.0.3" } } diff --git a/packages/bookshelf-filter/test/.eslintrc.js b/packages/bookshelf-filter/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/bookshelf-filter/test/.eslintrc.js +++ b/packages/bookshelf-filter/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/bookshelf-filter/test/bookshelf-filter.test.js b/packages/bookshelf-filter/test/bookshelf-filter.test.js index ceed3396b..a2f535c27 100644 --- a/packages/bookshelf-filter/test/bookshelf-filter.test.js +++ b/packages/bookshelf-filter/test/bookshelf-filter.test.js @@ -1,10 +1,10 @@ -const assert = require('node:assert/strict'); -const Module = require('node:module'); -const sinon = require('sinon'); -const errors = require('@tryghost/errors'); -const installPlugin = require('..'); +const assert = require("node:assert/strict"); +const Module = require("node:module"); +const sinon = require("sinon"); +const errors = require("@tryghost/errors"); +const installPlugin = require(".."); -describe('@tryghost/bookshelf-filter', function () { +describe("@tryghost/bookshelf-filter", function () { let Bookshelf; let ParentModel; @@ -19,7 +19,7 @@ describe('@tryghost/bookshelf-filter', function () { return Child; }; - Bookshelf = {Model: ParentModel}; + Bookshelf = { Model: ParentModel }; installPlugin(Bookshelf); }); @@ -27,15 +27,15 @@ describe('@tryghost/bookshelf-filter', function () { sinon.restore(); }); - it('exports plugin from index', function () { - assert.equal(typeof require('../index'), 'function'); + it("exports plugin from index", function () { + assert.equal(typeof require("../index"), "function"); }); - it('replaces Bookshelf.Model with extended model', function () { + it("replaces Bookshelf.Model with extended model", function () { assert.notEqual(Bookshelf.Model, ParentModel); }); - it('provides default no-op filter methods', function () { + it("provides default no-op filter methods", function () { const model = new Bookshelf.Model(); assert.equal(model.enforcedFilters(), undefined); assert.equal(model.defaultFilters(), undefined); @@ -45,53 +45,53 @@ describe('@tryghost/bookshelf-filter', function () { assert.equal(model._filters, null); }); - it('applies composed filters and forwards nql options', function () { - const qb = {where: sinon.stub()}; + it("applies composed filters and forwards nql options", function () { + const qb = { where: sinon.stub() }; const querySQL = sinon.stub(); - const nqlStub = sinon.stub().returns({querySQL}); - sinon.stub(Module, '_load').callsFake((request, parent, isMain) => { - if (request === '@tryghost/nql') { + const nqlStub = sinon.stub().returns({ querySQL }); + sinon.stub(Module, "_load").callsFake((request, parent, isMain) => { + if (request === "@tryghost/nql") { return nqlStub; } return Module._load.wrappedMethod.call(Module, request, parent, isMain); }); const model = new Bookshelf.Model(); - model.filterExpansions = sinon.stub().returns(['posts.tags']); - model.extraFilters = sinon.stub().returns('status:published'); - model.enforcedFilters = sinon.stub().returns('visibility:public'); - model.defaultFilters = sinon.stub().returns('type:post'); - model.filterRelations = sinon.stub().returns({authors: {tableName: 'users'}}); - model.query = sinon.stub().callsFake(fn => fn(qb)); + model.filterExpansions = sinon.stub().returns(["posts.tags"]); + model.extraFilters = sinon.stub().returns("status:published"); + model.enforcedFilters = sinon.stub().returns("visibility:public"); + model.defaultFilters = sinon.stub().returns("type:post"); + model.filterRelations = sinon.stub().returns({ authors: { tableName: "users" } }); + model.query = sinon.stub().callsFake((fn) => fn(qb)); const options = { - filter: 'title:test', + filter: "title:test", useCTE: true, - mongoTransformer: sinon.stub() + mongoTransformer: sinon.stub(), }; model.applyDefaultAndCustomFilters(options); assert.equal(model.query.calledOnce, true); assert.equal(nqlStub.calledOnce, true); - assert.equal(nqlStub.firstCall.args[0], 'title:test+status:published'); + assert.equal(nqlStub.firstCall.args[0], "title:test+status:published"); assert.deepEqual(nqlStub.firstCall.args[1], { - relations: {authors: {tableName: 'users'}}, - expansions: ['posts.tags'], - overrides: 'visibility:public', - defaults: 'type:post', + relations: { authors: { tableName: "users" } }, + expansions: ["posts.tags"], + overrides: "visibility:public", + defaults: "type:post", transformer: options.mongoTransformer, - cte: true + cte: true, }); assert.equal(querySQL.calledOnceWithExactly(qb), true); }); - it('uses extra filter as custom filter when custom is missing', function () { + it("uses extra filter as custom filter when custom is missing", function () { const qb = {}; const querySQL = sinon.stub(); - const nqlStub = sinon.stub().returns({querySQL}); - sinon.stub(Module, '_load').callsFake((request, parent, isMain) => { - if (request === '@tryghost/nql') { + const nqlStub = sinon.stub().returns({ querySQL }); + sinon.stub(Module, "_load").callsFake((request, parent, isMain) => { + if (request === "@tryghost/nql") { return nqlStub; } return Module._load.wrappedMethod.call(Module, request, parent, isMain); @@ -99,46 +99,49 @@ describe('@tryghost/bookshelf-filter', function () { const model = new Bookshelf.Model(); model.filterExpansions = sinon.stub().returns(undefined); - model.extraFilters = sinon.stub().returns('status:published'); + model.extraFilters = sinon.stub().returns("status:published"); model.enforcedFilters = sinon.stub().returns(undefined); model.defaultFilters = sinon.stub().returns(undefined); model.filterRelations = sinon.stub().returns(undefined); - model.query = sinon.stub().callsFake(fn => fn(qb)); + model.query = sinon.stub().callsFake((fn) => fn(qb)); model.applyDefaultAndCustomFilters({ - useCTE: false + useCTE: false, }); - assert.equal(nqlStub.firstCall.args[0], 'status:published'); + assert.equal(nqlStub.firstCall.args[0], "status:published"); assert.deepEqual(nqlStub.firstCall.args[1], { relations: {}, expansions: [], overrides: undefined, defaults: undefined, transformer: undefined, - cte: false + cte: false, }); }); - it('wraps parser errors in BadRequestError', function () { - const rootError = new Error('parse failed'); + it("wraps parser errors in BadRequestError", function () { + const rootError = new Error("parse failed"); const nqlStub = sinon.stub().throws(rootError); - sinon.stub(Module, '_load').callsFake((request, parent, isMain) => { - if (request === '@tryghost/nql') { + sinon.stub(Module, "_load").callsFake((request, parent, isMain) => { + if (request === "@tryghost/nql") { return nqlStub; } return Module._load.wrappedMethod.call(Module, request, parent, isMain); }); const model = new Bookshelf.Model(); - model.query = sinon.stub().callsFake(fn => fn({})); - - assert.throws(() => { - model.applyDefaultAndCustomFilters({filter: 'bad'}); - }, (err) => { - assert.equal(err instanceof errors.BadRequestError, true); - assert.equal(err.message, 'Error parsing filter'); - return true; - }); + model.query = sinon.stub().callsFake((fn) => fn({})); + + assert.throws( + () => { + model.applyDefaultAndCustomFilters({ filter: "bad" }); + }, + (err) => { + assert.equal(err instanceof errors.BadRequestError, true); + assert.equal(err.message, "Error parsing filter"); + return true; + }, + ); }); }); diff --git a/packages/bookshelf-has-posts/README.md b/packages/bookshelf-has-posts/README.md index 4df97f430..8fcd2ec1c 100644 --- a/packages/bookshelf-has-posts/README.md +++ b/packages/bookshelf-has-posts/README.md @@ -8,36 +8,30 @@ or `yarn add @tryghost/bookshelf-has-posts` - ## Purpose Bookshelf plugin that restricts model queries to records associated with published posts. ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests +# Copyright & License - - -# Copyright & License - -Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/bookshelf-has-posts/index.js b/packages/bookshelf-has-posts/index.js index 53c297a3b..ca85edbff 100644 --- a/packages/bookshelf-has-posts/index.js +++ b/packages/bookshelf-has-posts/index.js @@ -1 +1 @@ -module.exports = require('./lib/bookshelf-has-posts'); +module.exports = require("./lib/bookshelf-has-posts"); diff --git a/packages/bookshelf-has-posts/lib/bookshelf-has-posts.js b/packages/bookshelf-has-posts/lib/bookshelf-has-posts.js index 8b368b717..475b61d7a 100644 --- a/packages/bookshelf-has-posts/lib/bookshelf-has-posts.js +++ b/packages/bookshelf-has-posts/lib/bookshelf-has-posts.js @@ -1,18 +1,17 @@ -const _ = require('lodash'); -const _debug = require('@tryghost/debug')._base; -const debug = _debug('ghost-query'); +const _ = require("lodash"); +const _debug = require("@tryghost/debug")._base; +const debug = _debug("ghost-query"); const addHasPostsWhere = (tableName, config) => { const comparisonField = `${tableName}.id`; return function (qb) { return qb.whereIn(comparisonField, function () { - const innerQb = this - .distinct(`${config.joinTable}.${config.joinTo}`) + const innerQb = this.distinct(`${config.joinTable}.${config.joinTo}`) .select() .from(config.joinTable) - .join('posts', 'posts.id', `${config.joinTable}.post_id`) - .andWhere('posts.status', '=', 'published'); + .join("posts", "posts.id", `${config.joinTable}.post_id`) + .andWhere("posts.status", "=", "published"); debug(`QUERY has posts: ${innerQb.toSQL().sql}`); @@ -34,11 +33,11 @@ const hasPosts = function hasPosts(Bookshelf) { fetch: function () { if (this.shouldHavePosts) { - this.query(addHasPostsWhere(_.result(this, 'tableName'), this.shouldHavePosts)); + this.query(addHasPostsWhere(_.result(this, "tableName"), this.shouldHavePosts)); } - if (_debug.enabled('ghost-query')) { - debug('QUERY', this.query().toQuery()); + if (_debug.enabled("ghost-query")) { + debug("QUERY", this.query().toQuery()); } return modelPrototype.fetch.apply(this, arguments); @@ -46,15 +45,15 @@ const hasPosts = function hasPosts(Bookshelf) { fetchAll: function () { if (this.shouldHavePosts) { - this.query(addHasPostsWhere(_.result(this, 'tableName'), this.shouldHavePosts)); + this.query(addHasPostsWhere(_.result(this, "tableName"), this.shouldHavePosts)); } - if (_debug.enabled('ghost-query')) { - debug('QUERY', this.query().toQuery()); + if (_debug.enabled("ghost-query")) { + debug("QUERY", this.query().toQuery()); } return modelPrototype.fetchAll.apply(this, arguments); - } + }, }); }; diff --git a/packages/bookshelf-has-posts/package.json b/packages/bookshelf-has-posts/package.json index 276807e7b..08b752172 100644 --- a/packages/bookshelf-has-posts/package.json +++ b/packages/bookshelf-has-posts/package.json @@ -1,33 +1,33 @@ { "name": "@tryghost/bookshelf-has-posts", "version": "2.1.0", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/bookshelf-has-posts" }, - "author": "Ghost Foundation", - "license": "MIT", - "main": "index.js", - "scripts": { - "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" - }, "files": [ "index.js", "lib" ], + "main": "index.js", "publishConfig": { "access": "public" }, - "devDependencies": { - "sinon": "21.0.3" + "scripts": { + "dev": "echo \"Implement me!\"", + "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "dependencies": { "@tryghost/debug": "^2.0.3", "lodash": "4.17.23" + }, + "devDependencies": { + "sinon": "21.0.3" } } diff --git a/packages/bookshelf-has-posts/test/.eslintrc.js b/packages/bookshelf-has-posts/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/bookshelf-has-posts/test/.eslintrc.js +++ b/packages/bookshelf-has-posts/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/bookshelf-has-posts/test/bookshelf-has-posts.test.js b/packages/bookshelf-has-posts/test/bookshelf-has-posts.test.js index d572e5017..5ea654a60 100644 --- a/packages/bookshelf-has-posts/test/bookshelf-has-posts.test.js +++ b/packages/bookshelf-has-posts/test/bookshelf-has-posts.test.js @@ -1,9 +1,9 @@ -const assert = require('node:assert/strict'); -const sinon = require('sinon'); -const debugBase = require('@tryghost/debug')._base; -const plugin = require('..'); +const assert = require("node:assert/strict"); +const sinon = require("sinon"); +const debugBase = require("@tryghost/debug")._base; +const plugin = require(".."); -describe('@tryghost/bookshelf-has-posts', function () { +describe("@tryghost/bookshelf-has-posts", function () { let Bookshelf; let ParentModel; let initStub; @@ -11,9 +11,9 @@ describe('@tryghost/bookshelf-has-posts', function () { let fetchAllStub; beforeEach(function () { - initStub = sinon.stub().returns('INIT'); - fetchStub = sinon.stub().returns('FETCH'); - fetchAllStub = sinon.stub().returns('FETCH_ALL'); + initStub = sinon.stub().returns("INIT"); + fetchStub = sinon.stub().returns("FETCH"); + fetchAllStub = sinon.stub().returns("FETCH_ALL"); ParentModel = function BaseModel() {}; ParentModel.prototype.initialize = initStub; @@ -29,7 +29,7 @@ describe('@tryghost/bookshelf-has-posts', function () { return Child; }; - Bookshelf = {Model: ParentModel}; + Bookshelf = { Model: ParentModel }; plugin(Bookshelf); }); @@ -37,32 +37,32 @@ describe('@tryghost/bookshelf-has-posts', function () { sinon.restore(); }); - it('exports plugin and helper', function () { - assert.equal(typeof require('../index'), 'function'); - assert.equal(typeof plugin.addHasPostsWhere, 'function'); + it("exports plugin and helper", function () { + assert.equal(typeof require("../index"), "function"); + assert.equal(typeof plugin.addHasPostsWhere, "function"); }); - it('initialize delegates to parent initialize', function () { + it("initialize delegates to parent initialize", function () { const model = new Bookshelf.Model(); - const result = model.initialize('arg'); - assert.equal(result, 'INIT'); - assert.equal(initStub.calledOnceWithExactly('arg'), true); + const result = model.initialize("arg"); + assert.equal(result, "INIT"); + assert.equal(initStub.calledOnceWithExactly("arg"), true); }); - it('addHasPostsWhere builds expected query shape', function () { + it("addHasPostsWhere builds expected query shape", function () { const whereIn = sinon.stub(); - const qb = {whereIn}; - const tableName = 'tags'; + const qb = { whereIn }; + const tableName = "tags"; const config = { - joinTable: 'posts_tags', - joinTo: 'tag_id' + joinTable: "posts_tags", + joinTo: "tag_id", }; const whereFn = plugin.addHasPostsWhere(tableName, config); whereFn(qb); assert.equal(whereIn.calledOnce, true); - assert.equal(whereIn.firstCall.args[0], 'tags.id'); + assert.equal(whereIn.firstCall.args[0], "tags.id"); const subqueryBuilder = { distinct: sinon.stub().returnsThis(), @@ -71,24 +71,30 @@ describe('@tryghost/bookshelf-has-posts', function () { whereRaw: sinon.stub().returnsThis(), join: sinon.stub().returnsThis(), andWhere: sinon.stub().returnsThis(), - toSQL: sinon.stub().returns({sql: 'select *'}) + toSQL: sinon.stub().returns({ sql: "select *" }), }; const callbackResult = whereIn.firstCall.args[1].call(subqueryBuilder); assert.equal(callbackResult, subqueryBuilder); - assert.equal(subqueryBuilder.distinct.calledOnceWithExactly('posts_tags.tag_id'), true); - assert.equal(subqueryBuilder.from.calledOnceWithExactly('posts_tags'), true); - assert.equal(subqueryBuilder.join.calledOnceWithExactly('posts', 'posts.id', 'posts_tags.post_id'), true); - assert.equal(subqueryBuilder.andWhere.calledOnceWithExactly('posts.status', '=', 'published'), true); + assert.equal(subqueryBuilder.distinct.calledOnceWithExactly("posts_tags.tag_id"), true); + assert.equal(subqueryBuilder.from.calledOnceWithExactly("posts_tags"), true); + assert.equal( + subqueryBuilder.join.calledOnceWithExactly("posts", "posts.id", "posts_tags.post_id"), + true, + ); + assert.equal( + subqueryBuilder.andWhere.calledOnceWithExactly("posts.status", "=", "published"), + true, + ); }); - it('addHasPostsWhere does not use a correlated subquery', function () { + it("addHasPostsWhere does not use a correlated subquery", function () { const whereIn = sinon.stub(); - const qb = {whereIn}; - const tableName = 'tags'; + const qb = { whereIn }; + const tableName = "tags"; const config = { - joinTable: 'posts_tags', - joinTo: 'tag_id' + joinTable: "posts_tags", + joinTo: "tag_id", }; const whereFn = plugin.addHasPostsWhere(tableName, config); @@ -101,7 +107,7 @@ describe('@tryghost/bookshelf-has-posts', function () { whereRaw: sinon.stub().returnsThis(), join: sinon.stub().returnsThis(), andWhere: sinon.stub().returnsThis(), - toSQL: sinon.stub().returns({sql: 'select *'}) + toSQL: sinon.stub().returns({ sql: "select *" }), }; whereIn.firstCall.args[1].call(subqueryBuilder); @@ -110,60 +116,63 @@ describe('@tryghost/bookshelf-has-posts', function () { // A correlated whereRaw (e.g. `posts_tags.tag_id = tags.id`) forces MySQL to // re-evaluate the subquery for every row in the outer table, causing excessive // row scanning on sites with many tags/authors. - assert.equal(subqueryBuilder.whereRaw.called, false, - 'subquery should not use whereRaw — correlated subqueries cause O(n²) row scanning'); + assert.equal( + subqueryBuilder.whereRaw.called, + false, + "subquery should not use whereRaw — correlated subqueries cause O(n²) row scanning", + ); }); - it('fetch applies has-posts query when configured', function () { - sinon.stub(debugBase, 'enabled').returns(false); + it("fetch applies has-posts query when configured", function () { + sinon.stub(debugBase, "enabled").returns(false); const model = new Bookshelf.Model(); - model.tableName = 'tags'; - model.shouldHavePosts = {joinTable: 'posts_tags', joinTo: 'tag_id'}; - model.query = sinon.stub().returns({toQuery: sinon.stub().returns('SQL')}); + model.tableName = "tags"; + model.shouldHavePosts = { joinTable: "posts_tags", joinTo: "tag_id" }; + model.query = sinon.stub().returns({ toQuery: sinon.stub().returns("SQL") }); const result = model.fetch({}); - assert.equal(result, 'FETCH'); + assert.equal(result, "FETCH"); assert.equal(model.query.calledOnce, true); assert.equal(fetchStub.calledOnce, true); }); - it('fetch skips has-posts query when not configured', function () { - sinon.stub(debugBase, 'enabled').returns(false); + it("fetch skips has-posts query when not configured", function () { + sinon.stub(debugBase, "enabled").returns(false); const model = new Bookshelf.Model(); - model.query = sinon.stub().returns({toQuery: sinon.stub().returns('SQL')}); + model.query = sinon.stub().returns({ toQuery: sinon.stub().returns("SQL") }); model.fetch({}); assert.equal(model.query.called, false); assert.equal(fetchStub.calledOnce, true); }); - it('fetch logs query when debug is enabled', function () { - sinon.stub(debugBase, 'enabled').returns(true); + it("fetch logs query when debug is enabled", function () { + sinon.stub(debugBase, "enabled").returns(true); const model = new Bookshelf.Model(); - model.query = sinon.stub().returns({toQuery: sinon.stub().returns('SELECT 1')}); + model.query = sinon.stub().returns({ toQuery: sinon.stub().returns("SELECT 1") }); const result = model.fetch({}); - assert.equal(result, 'FETCH'); + assert.equal(result, "FETCH"); assert.equal(model.query.calledOnce, true); }); - it('fetchAll applies has-posts query and logs in debug mode', function () { - sinon.stub(debugBase, 'enabled').returns(true); + it("fetchAll applies has-posts query and logs in debug mode", function () { + sinon.stub(debugBase, "enabled").returns(true); const model = new Bookshelf.Model(); - model.tableName = 'authors'; - model.shouldHavePosts = {joinTable: 'posts_authors', joinTo: 'author_id'}; - model.query = sinon.stub().returns({toQuery: sinon.stub().returns('SELECT 2')}); + model.tableName = "authors"; + model.shouldHavePosts = { joinTable: "posts_authors", joinTo: "author_id" }; + model.query = sinon.stub().returns({ toQuery: sinon.stub().returns("SELECT 2") }); const result = model.fetchAll({}); - assert.equal(result, 'FETCH_ALL'); + assert.equal(result, "FETCH_ALL"); assert.equal(model.query.calledTwice, true); assert.equal(fetchAllStub.calledOnce, true); }); - it('fetchAll skips query decoration when shouldHavePosts is missing', function () { - sinon.stub(debugBase, 'enabled').returns(false); + it("fetchAll skips query decoration when shouldHavePosts is missing", function () { + sinon.stub(debugBase, "enabled").returns(false); const model = new Bookshelf.Model(); - model.query = sinon.stub().returns({toQuery: sinon.stub().returns('SELECT 3')}); + model.query = sinon.stub().returns({ toQuery: sinon.stub().returns("SELECT 3") }); model.fetchAll({}); assert.equal(model.query.called, false); diff --git a/packages/bookshelf-include-count/README.md b/packages/bookshelf-include-count/README.md index e26c957b4..3424750d1 100644 --- a/packages/bookshelf-include-count/README.md +++ b/packages/bookshelf-include-count/README.md @@ -8,36 +8,30 @@ or `yarn add @tryghost/bookshelf-include-count` - ## Purpose Bookshelf plugin that augments fetch results with relation counts requested through `withRelated`. ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests +# Copyright & License - - -# Copyright & License - -Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/bookshelf-include-count/index.js b/packages/bookshelf-include-count/index.js index 5456c0966..48fe02b0c 100644 --- a/packages/bookshelf-include-count/index.js +++ b/packages/bookshelf-include-count/index.js @@ -1 +1 @@ -module.exports = require('./lib/bookshelf-include-count'); +module.exports = require("./lib/bookshelf-include-count"); diff --git a/packages/bookshelf-include-count/lib/bookshelf-include-count.js b/packages/bookshelf-include-count/lib/bookshelf-include-count.js index 70216eb4d..109a0051f 100644 --- a/packages/bookshelf-include-count/lib/bookshelf-include-count.js +++ b/packages/bookshelf-include-count/lib/bookshelf-include-count.js @@ -1,6 +1,6 @@ -const _debug = require('@tryghost/debug')._base; -const debug = _debug('ghost-query'); -const _ = require('lodash'); +const _debug = require("@tryghost/debug")._base; +const debug = _debug("ghost-query"); +const _ = require("lodash"); /** * @param {import('bookshelf')} Bookshelf @@ -26,7 +26,7 @@ module.exports = function (Bookshelf) { function hasWithRelated(key) { for (const item of options.withRelated) { - if (typeof item !== 'string') { + if (typeof item !== "string") { if (item[key] !== undefined) { return true; } @@ -43,7 +43,7 @@ module.exports = function (Bookshelf) { // We need to keep the reference to the withRelated array and not create a new array // This is required to make eager relations work correctly (otherwise the updated withRelated won't get passed further) const newItems = options.withRelated.filter((item) => { - if (typeof item === 'string') { + if (typeof item === "string") { return item !== key; } return item[key] === undefined; @@ -61,10 +61,10 @@ module.exports = function (Bookshelf) { if (model.countRelations) { const countRelations = model.countRelations(); for (const countRelation of Object.keys(countRelations)) { - if (hasWithRelated('count.' + countRelation)) { + if (hasWithRelated("count." + countRelation)) { // remove post_count from withRelated - removeWithRelated('count.' + countRelation); - + removeWithRelated("count." + countRelation); + // Call the query builder countRelations[countRelation](this, options); } @@ -96,12 +96,12 @@ module.exports = function (Bookshelf) { * E.g. when trying to load counts on replies.count.likes, we wouldn't get an opportunity to load the counts on the replies relation. */ sync: function (options) { - if (!options.method || (options.method !== 'insert' && options.method !== 'update')) { + if (!options.method || (options.method !== "insert" && options.method !== "update")) { this.addCounts.apply(this, arguments); } - if (_debug.enabled('ghost-query')) { - debug('QUERY', this.query().toQuery()); + if (_debug.enabled("ghost-query")) { + debug("QUERY", this.query().toQuery()); } // Call parent fetchAll @@ -121,7 +121,7 @@ module.exports = function (Bookshelf) { savedAttributes[key] = this.attributes[key]; } } - + return modelProto.save.apply(this, arguments).then((t) => { // Set savedAttributes, but keep count__ variables if they stayed inside this.attributes if (savedAttributes) { @@ -129,7 +129,7 @@ module.exports = function (Bookshelf) { } return t; }); - } + }, }); Bookshelf.Model = Model; @@ -142,13 +142,13 @@ module.exports = function (Bookshelf) { // For now, only apply this for eager loaded collections this.addCounts.apply(this, arguments); - if (_debug.enabled('ghost-query')) { - debug('QUERY', this.query().toQuery()); + if (_debug.enabled("ghost-query")) { + debug("QUERY", this.query().toQuery()); } // Call parent fetchAll return collectionProto.sync.apply(this, arguments); - } + }, }); Bookshelf.Collection = Collection; diff --git a/packages/bookshelf-include-count/package.json b/packages/bookshelf-include-count/package.json index 21b717c68..0c2bf2712 100644 --- a/packages/bookshelf-include-count/package.json +++ b/packages/bookshelf-include-count/package.json @@ -1,33 +1,33 @@ { "name": "@tryghost/bookshelf-include-count", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/bookshelf-include-count" }, - "author": "Ghost Foundation", - "license": "MIT", - "main": "index.js", - "scripts": { - "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" - }, "files": [ "index.js", "lib" ], + "main": "index.js", "publishConfig": { "access": "public" }, - "devDependencies": { - "sinon": "21.0.3" + "scripts": { + "dev": "echo \"Implement me!\"", + "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "dependencies": { "@tryghost/debug": "^2.0.3", "lodash": "4.17.23" + }, + "devDependencies": { + "sinon": "21.0.3" } } diff --git a/packages/bookshelf-include-count/test/.eslintrc.js b/packages/bookshelf-include-count/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/bookshelf-include-count/test/.eslintrc.js +++ b/packages/bookshelf-include-count/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/bookshelf-include-count/test/bookshelf-include-count.test.js b/packages/bookshelf-include-count/test/bookshelf-include-count.test.js index 868061799..d6ab76793 100644 --- a/packages/bookshelf-include-count/test/bookshelf-include-count.test.js +++ b/packages/bookshelf-include-count/test/bookshelf-include-count.test.js @@ -1,9 +1,9 @@ -const assert = require('node:assert/strict'); -const sinon = require('sinon'); -const debugBase = require('@tryghost/debug')._base; -const installPlugin = require('..'); +const assert = require("node:assert/strict"); +const sinon = require("sinon"); +const debugBase = require("@tryghost/debug")._base; +const installPlugin = require(".."); -describe('@tryghost/bookshelf-include-count', function () { +describe("@tryghost/bookshelf-include-count", function () { let Bookshelf; let BaseModel; let BaseCollection; @@ -16,9 +16,9 @@ describe('@tryghost/bookshelf-include-count', function () { modelSerialize = sinon.stub().callsFake(function () { return Object.assign({}, this.attributes); }); - modelSync = sinon.stub().returns('MODEL_SYNC'); - modelSave = sinon.stub().resolves('MODEL_SAVE'); - collectionSync = sinon.stub().returns('COLLECTION_SYNC'); + modelSync = sinon.stub().returns("MODEL_SYNC"); + modelSave = sinon.stub().resolves("MODEL_SAVE"); + collectionSync = sinon.stub().returns("COLLECTION_SYNC"); BaseModel = function ModelConstructor() { this.attributes = {}; @@ -27,7 +27,9 @@ describe('@tryghost/bookshelf-include-count', function () { BaseModel.prototype.serialize = modelSerialize; BaseModel.prototype.sync = modelSync; BaseModel.prototype.save = modelSave; - BaseModel.prototype.query = sinon.stub().returns({toQuery: sinon.stub().returns('SELECT 1')}); + BaseModel.prototype.query = sinon + .stub() + .returns({ toQuery: sinon.stub().returns("SELECT 1") }); BaseModel.extend = function extend(proto) { function Child() { BaseModel.apply(this, arguments); @@ -44,7 +46,9 @@ describe('@tryghost/bookshelf-include-count', function () { this.constructor = BaseCollection; }; BaseCollection.prototype.sync = collectionSync; - BaseCollection.prototype.query = sinon.stub().returns({toQuery: sinon.stub().returns('SELECT 2')}); + BaseCollection.prototype.query = sinon + .stub() + .returns({ toQuery: sinon.stub().returns("SELECT 2") }); BaseCollection.extend = function extend(proto) { function Child() { BaseCollection.apply(this, arguments); @@ -59,7 +63,7 @@ describe('@tryghost/bookshelf-include-count', function () { Bookshelf = { Model: BaseModel, - Collection: BaseCollection + Collection: BaseCollection, }; installPlugin(Bookshelf); @@ -69,165 +73,162 @@ describe('@tryghost/bookshelf-include-count', function () { sinon.restore(); }); - it('exports plugin from index', function () { - assert.equal(typeof require('../index'), 'function'); + it("exports plugin from index", function () { + assert.equal(typeof require("../index"), "function"); }); - it('serialize nests count__ keys under count object', function () { + it("serialize nests count__ keys under count object", function () { const model = new Bookshelf.Model(); model.attributes = { - id: '1', + id: "1", count__posts: 3, count__members: 9, - title: 'hello' + title: "hello", }; const result = model.serialize({}); assert.deepEqual(result, { - id: '1', - title: 'hello', + id: "1", + title: "hello", count: { posts: 3, - members: 9 - } + members: 9, + }, }); }); - it('model sync applies counts for non-insert/update methods', function () { - sinon.stub(debugBase, 'enabled').returns(false); + it("model sync applies counts for non-insert/update methods", function () { + sinon.stub(debugBase, "enabled").returns(false); const model = new Bookshelf.Model(); - model.query = sinon.stub().returns({toQuery: sinon.stub().returns('SQL')}); + model.query = sinon.stub().returns({ toQuery: sinon.stub().returns("SQL") }); model.constructor.countRelations = sinon.stub().returns({ - posts: sinon.stub() + posts: sinon.stub(), }); - const options = {method: 'read', withRelated: ['count.posts']}; + const options = { method: "read", withRelated: ["count.posts"] }; const result = model.sync(options); - assert.equal(result, 'MODEL_SYNC'); + assert.equal(result, "MODEL_SYNC"); assert.equal(model.constructor.countRelations.calledOnce, true); assert.deepEqual(options.withRelated, []); assert.equal(modelSync.calledOnceWithExactly(options), true); }); - it('model sync skips addCounts for insert/update methods', function () { - sinon.stub(debugBase, 'enabled').returns(false); + it("model sync skips addCounts for insert/update methods", function () { + sinon.stub(debugBase, "enabled").returns(false); const model = new Bookshelf.Model(); model.constructor.countRelations = sinon.stub().returns({ - posts: sinon.stub() + posts: sinon.stub(), }); - const resultInsert = model.sync({method: 'insert', withRelated: ['count.posts']}); - const resultUpdate = model.sync({method: 'update', withRelated: ['count.posts']}); + const resultInsert = model.sync({ method: "insert", withRelated: ["count.posts"] }); + const resultUpdate = model.sync({ method: "update", withRelated: ["count.posts"] }); - assert.equal(resultInsert, 'MODEL_SYNC'); - assert.equal(resultUpdate, 'MODEL_SYNC'); + assert.equal(resultInsert, "MODEL_SYNC"); + assert.equal(resultUpdate, "MODEL_SYNC"); assert.equal(model.constructor.countRelations.called, false); }); - it('model sync logs query when debug is enabled', function () { - sinon.stub(debugBase, 'enabled').returns(true); + it("model sync logs query when debug is enabled", function () { + sinon.stub(debugBase, "enabled").returns(true); const model = new Bookshelf.Model(); - model.query = sinon.stub().returns({toQuery: sinon.stub().returns('SQL')}); + model.query = sinon.stub().returns({ toQuery: sinon.stub().returns("SQL") }); - const result = model.sync({method: 'read'}); + const result = model.sync({ method: "read" }); - assert.equal(result, 'MODEL_SYNC'); + assert.equal(result, "MODEL_SYNC"); assert.equal(model.query.calledOnce, true); }); - it('addCounts supports object withRelated entries and collection model fallback', function () { + it("addCounts supports object withRelated entries and collection model fallback", function () { const countLikes = sinon.stub(); const collection = new Bookshelf.Collection(); collection.model = { countRelations: sinon.stub().returns({ - likes: countLikes - }) + likes: countLikes, + }), }; const options = { - withRelated: [ - {foo: sinon.stub()}, - {'count.likes': sinon.stub()} - ] + withRelated: [{ foo: sinon.stub() }, { "count.likes": sinon.stub() }], }; collection.addCounts(options); assert.equal(countLikes.calledOnceWithExactly(collection, options), true); - assert.deepEqual(options.withRelated, [{foo: options.withRelated[0].foo}]); + assert.deepEqual(options.withRelated, [{ foo: options.withRelated[0].foo }]); }); - it('addCounts exits early for missing options/withRelated and absent countRelations', function () { + it("addCounts exits early for missing options/withRelated and absent countRelations", function () { const model = new Bookshelf.Model(); model.constructor.countRelations = null; assert.equal(model.addCounts(), undefined); assert.equal(model.addCounts({}), undefined); - assert.equal(model.addCounts({withRelated: ['count.posts']}), undefined); + assert.equal(model.addCounts({ withRelated: ["count.posts"] }), undefined); }); - it('addCounts does nothing when no relation key matches withRelated entries', function () { + it("addCounts does nothing when no relation key matches withRelated entries", function () { const countTags = sinon.stub(); const model = new Bookshelf.Model(); model.constructor.countRelations = sinon.stub().returns({ - tags: countTags + tags: countTags, }); const marker = sinon.stub(); const options = { - withRelated: ['author', {editor: marker}] + withRelated: ["author", { editor: marker }], }; model.addCounts(options); assert.equal(countTags.called, false); assert.equal(options.withRelated.length, 2); - assert.equal(options.withRelated[0], 'author'); - assert.deepEqual(options.withRelated[1], {editor: marker}); + assert.equal(options.withRelated[0], "author"); + assert.deepEqual(options.withRelated[1], { editor: marker }); }); - it('save preserves count__ attributes through save promise', async function () { + it("save preserves count__ attributes through save promise", async function () { const model = new Bookshelf.Model(); model.attributes = { - id: '1', + id: "1", count__posts: 4, - title: 'before' + title: "before", }; modelSave.callsFake(function () { model.attributes = { - id: '1', - title: 'after' + id: "1", + title: "after", }; - return Promise.resolve('MODEL_SAVE'); + return Promise.resolve("MODEL_SAVE"); }); - const result = await model.save({title: 'after'}); - assert.equal(result, 'MODEL_SAVE'); + const result = await model.save({ title: "after" }); + assert.equal(result, "MODEL_SAVE"); assert.deepEqual(model.attributes, { - id: '1', - title: 'after', - count__posts: 4 + id: "1", + title: "after", + count__posts: 4, }); }); - it('collection sync always applies counts and logs when debug enabled', function () { - sinon.stub(debugBase, 'enabled').returns(true); + it("collection sync always applies counts and logs when debug enabled", function () { + sinon.stub(debugBase, "enabled").returns(true); const collection = new Bookshelf.Collection(); const countTags = sinon.stub(); collection.model = { countRelations: sinon.stub().returns({ - tags: countTags - }) + tags: countTags, + }), }; - collection.query = sinon.stub().returns({toQuery: sinon.stub().returns('SELECT 10')}); + collection.query = sinon.stub().returns({ toQuery: sinon.stub().returns("SELECT 10") }); - const options = {withRelated: ['count.tags']}; + const options = { withRelated: ["count.tags"] }; const result = collection.sync(options); - assert.equal(result, 'COLLECTION_SYNC'); + assert.equal(result, "COLLECTION_SYNC"); assert.equal(countTags.calledOnceWithExactly(collection, options), true); assert.deepEqual(options.withRelated, []); assert.equal(collection.query.calledOnce, true); diff --git a/packages/bookshelf-order/README.md b/packages/bookshelf-order/README.md index cccc52497..3d043933b 100644 --- a/packages/bookshelf-order/README.md +++ b/packages/bookshelf-order/README.md @@ -8,36 +8,30 @@ or `yarn add @tryghost/bookshelf-order` - ## Purpose Bookshelf plugin that adds standardized ordering helpers to model queries. ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests +# Copyright & License - - -# Copyright & License - -Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/bookshelf-order/index.js b/packages/bookshelf-order/index.js index 39722f8a5..c86b20ad5 100644 --- a/packages/bookshelf-order/index.js +++ b/packages/bookshelf-order/index.js @@ -1 +1 @@ -module.exports = require('./lib/bookshelf-order'); +module.exports = require("./lib/bookshelf-order"); diff --git a/packages/bookshelf-order/lib/bookshelf-order.js b/packages/bookshelf-order/lib/bookshelf-order.js index 840779aef..c5afc77a9 100644 --- a/packages/bookshelf-order/lib/bookshelf-order.js +++ b/packages/bookshelf-order/lib/bookshelf-order.js @@ -1,4 +1,4 @@ -const _ = require('lodash'); +const _ = require("lodash"); /** * @param {import('bookshelf')} Bookshelf @@ -14,18 +14,18 @@ const orderPlugin = function orderPlugin(Bookshelf) { const eagerLoadArray = []; const orderAttributes = this.orderAttributes(); - if (withRelated && withRelated.indexOf('count.posts') > -1) { - orderAttributes.push('count.posts'); + if (withRelated && withRelated.indexOf("count.posts") > -1) { + orderAttributes.push("count.posts"); } let rules = []; // CASE: repeat order query parameter keys are present if (_.isArray(orderQueryString)) { orderQueryString.forEach((qs) => { - rules.push(...qs.split(',')); + rules.push(...qs.split(",")); }); } else { - rules = orderQueryString.split(','); + rules = orderQueryString.split(","); } _.each(rules, (rule) => { @@ -70,10 +70,10 @@ const orderPlugin = function orderPlugin(Bookshelf) { return { order, - orderRaw: orderRaw.join(', '), - eagerLoad: _.uniq(eagerLoadArray) + orderRaw: orderRaw.join(", "), + eagerLoad: _.uniq(eagerLoadArray), }; - } + }, }); }; diff --git a/packages/bookshelf-order/package.json b/packages/bookshelf-order/package.json index 0eaae94b6..28848cad6 100644 --- a/packages/bookshelf-order/package.json +++ b/packages/bookshelf-order/package.json @@ -1,14 +1,21 @@ { "name": "@tryghost/bookshelf-order", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/bookshelf-order" }, - "author": "Ghost Foundation", - "license": "MIT", + "files": [ + "index.js", + "lib" + ], "main": "index.js", + "publishConfig": { + "access": "public" + }, "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", @@ -16,17 +23,10 @@ "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, - "files": [ - "index.js", - "lib" - ], - "publishConfig": { - "access": "public" + "dependencies": { + "lodash": "4.17.23" }, "devDependencies": { "sinon": "21.0.3" - }, - "dependencies": { - "lodash": "4.17.23" } } diff --git a/packages/bookshelf-order/test/.eslintrc.js b/packages/bookshelf-order/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/bookshelf-order/test/.eslintrc.js +++ b/packages/bookshelf-order/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/bookshelf-order/test/bookshelf-order.test.js b/packages/bookshelf-order/test/bookshelf-order.test.js index b2783c3be..27be2764e 100644 --- a/packages/bookshelf-order/test/bookshelf-order.test.js +++ b/packages/bookshelf-order/test/bookshelf-order.test.js @@ -1,7 +1,7 @@ -const assert = require('node:assert/strict'); -const installPlugin = require('..'); +const assert = require("node:assert/strict"); +const installPlugin = require(".."); -describe('@tryghost/bookshelf-order', function () { +describe("@tryghost/bookshelf-order", function () { let Bookshelf; let ParentModel; @@ -16,109 +16,112 @@ describe('@tryghost/bookshelf-order', function () { return Child; }; - Bookshelf = {Model: ParentModel}; + Bookshelf = { Model: ParentModel }; installPlugin(Bookshelf); }); - it('exports plugin from index', function () { - assert.equal(typeof require('../index'), 'function'); + it("exports plugin from index", function () { + assert.equal(typeof require("../index"), "function"); }); - it('provides default no-op order hooks', function () { + it("provides default no-op order hooks", function () { const model = new Bookshelf.Model(); assert.equal(model.orderAttributes(), undefined); assert.equal(model.orderRawQuery(), undefined); }); - it('parses simple order rules and ignores invalid ones', function () { + it("parses simple order rules and ignores invalid ones", function () { const model = new Bookshelf.Model(); model.orderAttributes = function () { - return ['posts.created_at', 'posts.title']; + return ["posts.created_at", "posts.title"]; }; model.orderRawQuery = function () {}; - const result = model.parseOrderOption('created_at desc, invalid, title asc', []); + const result = model.parseOrderOption("created_at desc, invalid, title asc", []); assert.deepEqual(result, { order: { - 'posts.created_at': 'DESC', - 'posts.title': 'ASC' + "posts.created_at": "DESC", + "posts.title": "ASC", }, - orderRaw: '', - eagerLoad: [] + orderRaw: "", + eagerLoad: [], }); }); - it('supports repeated order query parameters and count.posts relation', function () { + it("supports repeated order query parameters and count.posts relation", function () { const model = new Bookshelf.Model(); model.orderAttributes = function () { - return ['posts.created_at']; + return ["posts.created_at"]; }; model.orderRawQuery = function () {}; - const result = model.parseOrderOption(['count.posts desc', 'created_at asc'], ['count.posts']); + const result = model.parseOrderOption( + ["count.posts desc", "created_at asc"], + ["count.posts"], + ); assert.deepEqual(result, { order: { - 'count.posts': 'DESC', - 'posts.created_at': 'ASC' + "count.posts": "DESC", + "posts.created_at": "ASC", }, - orderRaw: '', - eagerLoad: [] + orderRaw: "", + eagerLoad: [], }); }); - it('uses orderRawQuery with eagerLoad and deduplicates eager relations', function () { + it("uses orderRawQuery with eagerLoad and deduplicates eager relations", function () { const model = new Bookshelf.Model(); model.orderAttributes = function () { - return ['posts.title']; + return ["posts.title"]; }; model.orderRawQuery = function (field, direction) { - if (field === 'score') { + if (field === "score") { return { orderByRaw: `SCORE ${direction}`, - eagerLoad: 'authors' + eagerLoad: "authors", }; } - if (field === 'rank') { + if (field === "rank") { return { orderByRaw: `RANK ${direction}`, - eagerLoad: 'authors' + eagerLoad: "authors", }; } }; - const result = model.parseOrderOption('score asc, rank desc, title asc', []); + const result = model.parseOrderOption("score asc, rank desc, title asc", []); assert.deepEqual(result, { order: { - 'posts.title': 'ASC' + "posts.title": "ASC", }, - orderRaw: 'SCORE ASC, RANK DESC', - eagerLoad: ['authors'] + orderRaw: "SCORE ASC, RANK DESC", + eagerLoad: ["authors"], }); }); - it('ignores unknown order attributes and raw queries without eagerLoad', function () { + it("ignores unknown order attributes and raw queries without eagerLoad", function () { const model = new Bookshelf.Model(); model.orderAttributes = function () { - return ['posts.title']; + return ["posts.title"]; }; model.orderRawQuery = function (field, direction) { - if (field === 'custom') { + if (field === "custom") { return { - orderByRaw: `CUSTOM ${direction}` + orderByRaw: `CUSTOM ${direction}`, }; } }; - const result = model.parseOrderOption('unknown desc, custom asc', []); + const result = model.parseOrderOption("unknown desc, custom asc", []); assert.deepEqual(result, { order: {}, - orderRaw: 'CUSTOM ASC', - eagerLoad: [] + orderRaw: "CUSTOM ASC", + eagerLoad: [], }); }); }); diff --git a/packages/bookshelf-pagination/README.md b/packages/bookshelf-pagination/README.md index 21995bf5e..d9c9e217b 100644 --- a/packages/bookshelf-pagination/README.md +++ b/packages/bookshelf-pagination/README.md @@ -8,36 +8,30 @@ or `yarn add @tryghost/bookshelf-pagination` - ## Purpose Bookshelf plugin that adds pagination behavior and metadata to model collection queries. ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests +# Copyright & License - - -# Copyright & License - -Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/bookshelf-pagination/index.js b/packages/bookshelf-pagination/index.js index 3bd598d50..1aa8839b0 100644 --- a/packages/bookshelf-pagination/index.js +++ b/packages/bookshelf-pagination/index.js @@ -1 +1 @@ -module.exports = require('./lib/bookshelf-pagination'); +module.exports = require("./lib/bookshelf-pagination"); diff --git a/packages/bookshelf-pagination/lib/bookshelf-pagination.js b/packages/bookshelf-pagination/lib/bookshelf-pagination.js index a7669db67..4c8af013b 100644 --- a/packages/bookshelf-pagination/lib/bookshelf-pagination.js +++ b/packages/bookshelf-pagination/lib/bookshelf-pagination.js @@ -1,21 +1,22 @@ // # Pagination // // Extends Bookshelf.Model with a `fetchPage` method. Handles everything to do with paginated requests. -const _ = require('lodash'); +const _ = require("lodash"); -const errors = require('@tryghost/errors'); -const tpl = require('@tryghost/tpl'); +const errors = require("@tryghost/errors"); +const tpl = require("@tryghost/tpl"); const messages = { - pageNotFound: 'Page not found', - couldNotUnderstandRequest: 'Could not understand request.' + pageNotFound: "Page not found", + couldNotUnderstandRequest: "Could not understand request.", }; let defaults; let paginationUtils; const JOIN_KEYWORD_REGEX = /\bjoin\b/i; -const FROM_CLAUSE_REGEX = /\bfrom\b([\s\S]*?)(?:\bwhere\b|\bgroup\s+by\b|\border\s+by\b|\blimit\b|\boffset\b|$)/i; +const FROM_CLAUSE_REGEX = + /\bfrom\b([\s\S]*?)(?:\bwhere\b|\bgroup\s+by\b|\border\s+by\b|\blimit\b|\boffset\b|$)/i; function getCompiledSql(queryBuilder) { const compiledQuery = queryBuilder.toSQL(); @@ -23,18 +24,18 @@ function getCompiledSql(queryBuilder) { if (Array.isArray(compiledQuery)) { return compiledQuery .map((query) => { - return query && query.sql ? query.sql : ''; + return query && query.sql ? query.sql : ""; }) - .join(' '); + .join(" "); } - return compiledQuery && compiledQuery.sql ? compiledQuery.sql : ''; + return compiledQuery && compiledQuery.sql ? compiledQuery.sql : ""; } function extractFromClause(sql) { const fromClauseMatch = sql.match(FROM_CLAUSE_REGEX); - return fromClauseMatch ? fromClauseMatch[1] : ''; + return fromClauseMatch ? fromClauseMatch[1] : ""; } function hasJoinKeyword(sql) { @@ -42,7 +43,7 @@ function hasJoinKeyword(sql) { } function hasCommaSeparatedFromSources(sql) { - return extractFromClause(sql).includes(','); + return extractFromClause(sql).includes(","); } // Smart count only uses count(*) for single-table queries. @@ -66,7 +67,7 @@ function hasMultiTableSource(queryBuilder) { */ defaults = { page: 1, - limit: 15 + limit: 15, }; /** @@ -83,7 +84,7 @@ paginationUtils = { parseOptions: function parseOptions(options) { options = _.defaults(options || {}, defaults); - if (options.limit !== 'all') { + if (options.limit !== "all") { options.limit = parseInt(options.limit, 10) || defaults.limit; } @@ -99,9 +100,7 @@ paginationUtils = { */ addLimitAndOffset: function addLimitAndOffset(model, options) { if (_.isNumber(options.limit)) { - model - .query('limit', options.limit) - .query('offset', options.limit * (options.page - 1)); + model.query("limit", options.limit).query("offset", options.limit * (options.page - 1)); } }, @@ -121,7 +120,7 @@ paginationUtils = { pages: calcPages === 0 ? 1 : calcPages, total: totalItems, next: null, - prev: null + prev: null, }; if (pagination.pages > 1) { @@ -144,9 +143,9 @@ paginationUtils = { * @param {string} propertyName property to be inspected and included in the relation */ handleRelation: function handleRelation(model, propertyName) { - const tableName = _.result(model.constructor.prototype, 'tableName'); + const tableName = _.result(model.constructor.prototype, "tableName"); - const targetTable = propertyName.includes('.') && propertyName.split('.')[0]; + const targetTable = propertyName.includes(".") && propertyName.split(".")[0]; if (targetTable && targetTable !== tableName) { if (!model.eagerLoad) { @@ -157,7 +156,7 @@ paginationUtils = { model.eagerLoad.push(targetTable); } } - } + }, }; // ## Object Definitions @@ -212,9 +211,9 @@ const pagination = function pagination(bookshelf) { options = paginationUtils.parseOptions(options); // Get the table name and idAttribute for this model - const tableName = _.result(this.constructor.prototype, 'tableName'); + const tableName = _.result(this.constructor.prototype, "tableName"); - const idAttribute = _.result(this.constructor.prototype, 'idAttribute'); + const idAttribute = _.result(this.constructor.prototype, "idAttribute"); const self = this; // #### Pre count clauses @@ -224,97 +223,107 @@ const pagination = function pagination(bookshelf) { // Necessary due to lack of support for `count distinct` in bookshelf's count() // Skipped if limit='all' as we can use the length of the fetched data set let countPromise = Promise.resolve(); - if (options.limit !== 'all') { + if (options.limit !== "all") { const countQuery = this.query().clone(); if (options.transacting) { countQuery.transacting(options.transacting); } - countQuery.clear('select'); + countQuery.clear("select"); // Skipping distinct for simple queries where we know result rows are unique. const queryHasMultiTableSource = hasMultiTableSource(countQuery); if (options.useBasicCount || (options.useSmartCount && !queryHasMultiTableSource)) { - countPromise = countQuery.select( - bookshelf.knex.raw('count(*) as aggregate') - ); + countPromise = countQuery.select(bookshelf.knex.raw("count(*) as aggregate")); } else { countPromise = countQuery.select( - bookshelf.knex.raw('count(distinct ' + tableName + '.' + idAttribute + ') as aggregate') + bookshelf.knex.raw( + "count(distinct " + tableName + "." + idAttribute + ") as aggregate", + ), ); } } - return countPromise.then(function (countResult) { - // #### Post count clauses - // Add any where or join clauses which need to NOT be included with the aggregate query - - // Setup the pagination parameters so that we return the correct items from the set - paginationUtils.addLimitAndOffset(self, options); - - // Apply ordering options if they are present - if (options.order && !_.isEmpty(options.order)) { - _.forOwn(options.order, function (direction, property) { - if (property === 'count.posts') { - self.query('orderBy', 'count__posts', direction); - } else { - self.query('orderBy', property, direction); - - paginationUtils.handleRelation(self, property); - } - }); - } - - if (options.orderRaw) { - self.query((qb) => { - qb.orderByRaw(options.orderRaw, options.orderRawBindings); - }); - } - - if (!_.isEmpty(options.eagerLoad)) { - options.eagerLoad.forEach(property => paginationUtils.handleRelation(self, property)); - } - - if (options.groups && !_.isEmpty(options.groups)) { - _.each(options.groups, function (group) { - self.query('groupBy', group); - }); - } - - // Setup the promise to do a fetch on our collection, running the specified query - // @TODO: ensure option handling is done using an explicit pick elsewhere - - return self.fetchAll(_.omit(options, ['page', 'limit'])) - .then(function (fetchResult) { - if (options.limit === 'all') { - countResult = [{aggregate: fetchResult.length}]; - } - - return { - collection: fetchResult, - pagination: paginationUtils.formatResponse(countResult[0] ? countResult[0].aggregate : 0, options) - }; - }) - .catch(function (err) { - // e.g. offset/limit reached max allowed integer value - if (err.errno === 20 || err.errno === 1064) { - throw new errors.NotFoundError({message: tpl(messages.pageNotFound)}); - } - - throw err; - }); - }).catch((err) => { - // CASE: SQL syntax is incorrect - if (err.errno === 1054 || err.errno === 1) { - throw new errors.BadRequestError({ - message: tpl(messages.couldNotUnderstandRequest), - err - }); - } - - throw err; - }); - } + return countPromise + .then(function (countResult) { + // #### Post count clauses + // Add any where or join clauses which need to NOT be included with the aggregate query + + // Setup the pagination parameters so that we return the correct items from the set + paginationUtils.addLimitAndOffset(self, options); + + // Apply ordering options if they are present + if (options.order && !_.isEmpty(options.order)) { + _.forOwn(options.order, function (direction, property) { + if (property === "count.posts") { + self.query("orderBy", "count__posts", direction); + } else { + self.query("orderBy", property, direction); + + paginationUtils.handleRelation(self, property); + } + }); + } + + if (options.orderRaw) { + self.query((qb) => { + qb.orderByRaw(options.orderRaw, options.orderRawBindings); + }); + } + + if (!_.isEmpty(options.eagerLoad)) { + options.eagerLoad.forEach((property) => + paginationUtils.handleRelation(self, property), + ); + } + + if (options.groups && !_.isEmpty(options.groups)) { + _.each(options.groups, function (group) { + self.query("groupBy", group); + }); + } + + // Setup the promise to do a fetch on our collection, running the specified query + // @TODO: ensure option handling is done using an explicit pick elsewhere + + return self + .fetchAll(_.omit(options, ["page", "limit"])) + .then(function (fetchResult) { + if (options.limit === "all") { + countResult = [{ aggregate: fetchResult.length }]; + } + + return { + collection: fetchResult, + pagination: paginationUtils.formatResponse( + countResult[0] ? countResult[0].aggregate : 0, + options, + ), + }; + }) + .catch(function (err) { + // e.g. offset/limit reached max allowed integer value + if (err.errno === 20 || err.errno === 1064) { + throw new errors.NotFoundError({ + message: tpl(messages.pageNotFound), + }); + } + + throw err; + }); + }) + .catch((err) => { + // CASE: SQL syntax is incorrect + if (err.errno === 1054 || err.errno === 1) { + throw new errors.BadRequestError({ + message: tpl(messages.couldNotUnderstandRequest), + err, + }); + } + + throw err; + }); + }, }); }; diff --git a/packages/bookshelf-pagination/package.json b/packages/bookshelf-pagination/package.json index 6d9270bdd..8e7b98393 100644 --- a/packages/bookshelf-pagination/package.json +++ b/packages/bookshelf-pagination/package.json @@ -1,34 +1,34 @@ { "name": "@tryghost/bookshelf-pagination", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/bookshelf-pagination" }, - "author": "Ghost Foundation", - "license": "MIT", - "main": "index.js", - "scripts": { - "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" - }, "files": [ "index.js", "lib" ], + "main": "index.js", "publishConfig": { "access": "public" }, - "devDependencies": { - "sinon": "21.0.3" + "scripts": { + "dev": "echo \"Implement me!\"", + "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "dependencies": { "@tryghost/errors": "^3.0.3", "@tryghost/tpl": "^2.0.3", "lodash": "4.17.23" + }, + "devDependencies": { + "sinon": "21.0.3" } } diff --git a/packages/bookshelf-pagination/test/.eslintrc.js b/packages/bookshelf-pagination/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/bookshelf-pagination/test/.eslintrc.js +++ b/packages/bookshelf-pagination/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/bookshelf-pagination/test/pagination.test.js b/packages/bookshelf-pagination/test/pagination.test.js index fc4e1f52b..40fe6e73e 100644 --- a/packages/bookshelf-pagination/test/pagination.test.js +++ b/packages/bookshelf-pagination/test/pagination.test.js @@ -1,22 +1,22 @@ -const assert = require('node:assert/strict'); -const errors = require('@tryghost/errors'); -const paginationPlugin = require('../lib/bookshelf-pagination'); +const assert = require("node:assert/strict"); +const errors = require("@tryghost/errors"); +const paginationPlugin = require("../lib/bookshelf-pagination"); -function createBookshelf({countRows, fetchResult, selectError, fetchError} = {}) { +function createBookshelf({ countRows, fetchResult, selectError, fetchError } = {}) { const modelState = { queryCalls: [], rawCalls: [], - fetchAllArgs: null + fetchAllArgs: null, }; const qb = { orderByRaw(sql, bindings) { - modelState.orderByRaw = {sql, bindings}; - } + modelState.orderByRaw = { sql, bindings }; + }, }; const countQuery = { - _sql: 'select * from `posts`', + _sql: "select * from `posts`", clone() { modelState.countCloned = true; return countQuery; @@ -34,22 +34,22 @@ function createBookshelf({countRows, fetchResult, selectError, fetchError} = {}) if (selectError) { return Promise.reject(selectError); } - return Promise.resolve(countRows || [{aggregate: 1}]); + return Promise.resolve(countRows || [{ aggregate: 1 }]); }, toSQL() { - return {sql: countQuery._sql}; - } + return { sql: countQuery._sql }; + }, }; function ModelConstructor() {} - ModelConstructor.prototype.tableName = 'posts'; - ModelConstructor.prototype.idAttribute = 'id'; + ModelConstructor.prototype.tableName = "posts"; + ModelConstructor.prototype.idAttribute = "id"; ModelConstructor.prototype.query = function (method, ...args) { if (arguments.length === 0) { return countQuery; } - if (typeof method === 'function') { + if (typeof method === "function") { method(qb); return this; } @@ -62,7 +62,7 @@ function createBookshelf({countRows, fetchResult, selectError, fetchError} = {}) if (fetchError) { return Promise.reject(fetchError); } - return Promise.resolve(fetchResult || [{id: 1}]); + return Promise.resolve(fetchResult || [{ id: 1 }]); }; const bookshelf = { @@ -71,56 +71,59 @@ function createBookshelf({countRows, fetchResult, selectError, fetchError} = {}) raw(sql) { modelState.rawCalls.push(sql); return sql; - } - } + }, + }, }; paginationPlugin(bookshelf); - return {bookshelf, modelState}; + return { bookshelf, modelState }; } -describe('@tryghost/bookshelf-pagination', function () { - it('internal parseOptions handles bad and all limits', function () { - assert.deepEqual(paginationPlugin.paginationUtils.parseOptions({limit: 'bad', page: 'bad'}), { - limit: 15, - page: 1 - }); +describe("@tryghost/bookshelf-pagination", function () { + it("internal parseOptions handles bad and all limits", function () { + assert.deepEqual( + paginationPlugin.paginationUtils.parseOptions({ limit: "bad", page: "bad" }), + { + limit: 15, + page: 1, + }, + ); - assert.deepEqual(paginationPlugin.paginationUtils.parseOptions({limit: 'all'}), { - limit: 'all', - page: 1 + assert.deepEqual(paginationPlugin.paginationUtils.parseOptions({ limit: "all" }), { + limit: "all", + page: 1, }); }); - it('internal formatResponse defaults page when missing', function () { - assert.deepEqual(paginationPlugin.paginationUtils.formatResponse(0, {limit: 15}), { + it("internal formatResponse defaults page when missing", function () { + assert.deepEqual(paginationPlugin.paginationUtils.formatResponse(0, { limit: 15 }), { page: 1, limit: 15, pages: 1, total: 0, next: null, - prev: null + prev: null, }); }); - it('exports plugin from index', function () { - assert.equal(typeof require('../index'), 'function'); + it("exports plugin from index", function () { + assert.equal(typeof require("../index"), "function"); }); - it('adds fetchPage to Model prototype', function () { - const {bookshelf} = createBookshelf(); - assert.equal(typeof bookshelf.Model.prototype.fetchPage, 'function'); + it("adds fetchPage to Model prototype", function () { + const { bookshelf } = createBookshelf(); + assert.equal(typeof bookshelf.Model.prototype.fetchPage, "function"); }); - it('applies defaults and returns pagination metadata', async function () { - const {bookshelf, modelState} = createBookshelf({countRows: [{aggregate: 44}]}); + it("applies defaults and returns pagination metadata", async function () { + const { bookshelf, modelState } = createBookshelf({ countRows: [{ aggregate: 44 }] }); const model = new bookshelf.Model(); const result = await model.fetchPage(); assert.deepEqual(modelState.queryCalls.slice(0, 2), [ - ['limit', 15], - ['offset', 0] + ["limit", 15], + ["offset", 0], ]); assert.deepEqual(result.pagination, { page: 1, @@ -128,91 +131,107 @@ describe('@tryghost/bookshelf-pagination', function () { pages: 3, total: 44, next: 2, - prev: null + prev: null, }); }); - it('supports order, orderRaw, groups and eagerLoad relation handling', async function () { - const {bookshelf, modelState} = createBookshelf({countRows: [{aggregate: 44}]}); + it("supports order, orderRaw, groups and eagerLoad relation handling", async function () { + const { bookshelf, modelState } = createBookshelf({ countRows: [{ aggregate: 44 }] }); const model = new bookshelf.Model(); await model.fetchPage({ page: 3, limit: 5, order: { - 'count.posts': 'DESC', - 'authors.name': 'ASC' + "count.posts": "DESC", + "authors.name": "ASC", }, - orderRaw: 'FIELD(status, ?, ?) DESC', - orderRawBindings: ['published', 'draft'], - eagerLoad: ['tiers.name'], - groups: ['posts.id'] + orderRaw: "FIELD(status, ?, ?) DESC", + orderRawBindings: ["published", "draft"], + eagerLoad: ["tiers.name"], + groups: ["posts.id"], }); - assert.equal(modelState.queryCalls.some(call => call[0] === 'orderBy' && call[1] === 'count__posts' && call[2] === 'DESC'), true); - assert.equal(modelState.queryCalls.some(call => call[0] === 'orderBy' && call[1] === 'authors.name' && call[2] === 'ASC'), true); - assert.equal(modelState.queryCalls.some(call => call[0] === 'groupBy' && call[1] === 'posts.id'), true); + assert.equal( + modelState.queryCalls.some( + (call) => call[0] === "orderBy" && call[1] === "count__posts" && call[2] === "DESC", + ), + true, + ); + assert.equal( + modelState.queryCalls.some( + (call) => call[0] === "orderBy" && call[1] === "authors.name" && call[2] === "ASC", + ), + true, + ); + assert.equal( + modelState.queryCalls.some((call) => call[0] === "groupBy" && call[1] === "posts.id"), + true, + ); assert.deepEqual(modelState.orderByRaw, { - sql: 'FIELD(status, ?, ?) DESC', - bindings: ['published', 'draft'] + sql: "FIELD(status, ?, ?) DESC", + bindings: ["published", "draft"], }); - assert.deepEqual(model.eagerLoad.sort(), ['authors', 'tiers'].sort()); + assert.deepEqual(model.eagerLoad.sort(), ["authors", "tiers"].sort()); }); - it('supports limit=all without count query', async function () { - const fetchResult = [{id: 1}, {id: 2}, {id: 3}, {id: 4}, {id: 5}]; - const {bookshelf, modelState} = createBookshelf({fetchResult}); + it("supports limit=all without count query", async function () { + const fetchResult = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }]; + const { bookshelf, modelState } = createBookshelf({ fetchResult }); const model = new bookshelf.Model(); - const result = await model.fetchPage({limit: 'all'}); + const result = await model.fetchPage({ limit: "all" }); assert.equal(modelState.countCloned, undefined); - assert.equal(modelState.queryCalls.some(call => call[0] === 'limit'), false); + assert.equal( + modelState.queryCalls.some((call) => call[0] === "limit"), + false, + ); assert.deepEqual(result.pagination, { page: 1, - limit: 'all', + limit: "all", pages: 1, total: 5, next: null, - prev: null + prev: null, }); }); - it('supports useBasicCount and transacting', async function () { - const {bookshelf, modelState} = createBookshelf({countRows: [{aggregate: 1}]}); + it("supports useBasicCount and transacting", async function () { + const { bookshelf, modelState } = createBookshelf({ countRows: [{ aggregate: 1 }] }); const model = new bookshelf.Model(); await model.fetchPage({ page: 2, limit: 10, useBasicCount: true, - transacting: 'trx' + transacting: "trx", }); - assert.equal(modelState.transacting, 'trx'); - assert.equal(modelState.rawCalls[0], 'count(*) as aggregate'); + assert.equal(modelState.transacting, "trx"); + assert.equal(modelState.rawCalls[0], "count(*) as aggregate"); }); - it('uses distinct count query by default', async function () { - const {bookshelf, modelState} = createBookshelf({countRows: [{aggregate: 1}]}); + it("uses distinct count query by default", async function () { + const { bookshelf, modelState } = createBookshelf({ countRows: [{ aggregate: 1 }] }); const model = new bookshelf.Model(); - await model.fetchPage({page: 2, limit: 10}); + await model.fetchPage({ page: 2, limit: 10 }); - assert.equal(modelState.rawCalls[0], 'count(distinct posts.id) as aggregate'); + assert.equal(modelState.rawCalls[0], "count(distinct posts.id) as aggregate"); }); - it('useSmartCount uses count(*) when no JOINs present', async function () { - const {bookshelf, modelState} = createBookshelf({countRows: [{aggregate: 1}]}); + it("useSmartCount uses count(*) when no JOINs present", async function () { + const { bookshelf, modelState } = createBookshelf({ countRows: [{ aggregate: 1 }] }); const model = new bookshelf.Model(); - await model.fetchPage({page: 1, limit: 10, useSmartCount: true}); + await model.fetchPage({ page: 1, limit: 10, useSmartCount: true }); - assert.equal(modelState.rawCalls[0], 'count(*) as aggregate'); + assert.equal(modelState.rawCalls[0], "count(*) as aggregate"); }); - it('useSmartCount uses count(*) when SQL has commas outside FROM', async function () { - const {bookshelf, modelState} = createBookshelf({countRows: [{aggregate: 1}]}); + it("useSmartCount uses count(*) when SQL has commas outside FROM", async function () { + const { bookshelf, modelState } = createBookshelf({ countRows: [{ aggregate: 1 }] }); const model = new bookshelf.Model(); // Simulate a typical single-table query with multiple selected columns @@ -220,24 +239,25 @@ describe('@tryghost/bookshelf-pagination', function () { model.query = function () { const qb = originalQuery.apply(this, arguments); if (arguments.length === 0) { - qb._sql = 'select `posts`.`id`, `posts`.`title` from `posts` where `posts`.`status` = ?'; + qb._sql = + "select `posts`.`id`, `posts`.`title` from `posts` where `posts`.`status` = ?"; } return qb; }; - await model.fetchPage({page: 1, limit: 10, useSmartCount: true}); + await model.fetchPage({ page: 1, limit: 10, useSmartCount: true }); - assert.equal(modelState.rawCalls[0], 'count(*) as aggregate'); + assert.equal(modelState.rawCalls[0], "count(*) as aggregate"); }); for (const [joinType, sql] of [ - ['leftJoin', 'select * from `posts` left join `tags` on `posts`.`id` = `tags`.`post_id`'], - ['rightJoin', 'select * from `posts` right join `tags` on `posts`.`id` = `tags`.`post_id`'], - ['innerJoin', 'select * from `posts` inner join `tags` on `posts`.`id` = `tags`.`post_id`'], - ['joinRaw', 'select * from `posts` LEFT JOIN tags ON posts.id = tags.post_id'] + ["leftJoin", "select * from `posts` left join `tags` on `posts`.`id` = `tags`.`post_id`"], + ["rightJoin", "select * from `posts` right join `tags` on `posts`.`id` = `tags`.`post_id`"], + ["innerJoin", "select * from `posts` inner join `tags` on `posts`.`id` = `tags`.`post_id`"], + ["joinRaw", "select * from `posts` LEFT JOIN tags ON posts.id = tags.post_id"], ]) { it(`useSmartCount uses distinct count when ${joinType} is present`, async function () { - const {bookshelf, modelState} = createBookshelf({countRows: [{aggregate: 1}]}); + const { bookshelf, modelState } = createBookshelf({ countRows: [{ aggregate: 1 }] }); const model = new bookshelf.Model(); // Simulate a JOIN in the compiled SQL @@ -250,111 +270,123 @@ describe('@tryghost/bookshelf-pagination', function () { return qb; }; - await model.fetchPage({page: 1, limit: 10, useSmartCount: true}); + await model.fetchPage({ page: 1, limit: 10, useSmartCount: true }); - assert.equal(modelState.rawCalls[0], 'count(distinct posts.id) as aggregate'); + assert.equal(modelState.rawCalls[0], "count(distinct posts.id) as aggregate"); }); } - it('useSmartCount uses distinct count when comma-separated FROM sources are present', async function () { - const {bookshelf, modelState} = createBookshelf({countRows: [{aggregate: 1}]}); + it("useSmartCount uses distinct count when comma-separated FROM sources are present", async function () { + const { bookshelf, modelState } = createBookshelf({ countRows: [{ aggregate: 1 }] }); const model = new bookshelf.Model(); const originalQuery = model.query; model.query = function () { const qb = originalQuery.apply(this, arguments); if (arguments.length === 0) { - qb._sql = 'select * from `posts`, `tags` where `posts`.`id` = `tags`.`post_id`'; + qb._sql = "select * from `posts`, `tags` where `posts`.`id` = `tags`.`post_id`"; } return qb; }; - await model.fetchPage({page: 1, limit: 10, useSmartCount: true}); + await model.fetchPage({ page: 1, limit: 10, useSmartCount: true }); - assert.equal(modelState.rawCalls[0], 'count(distinct posts.id) as aggregate'); + assert.equal(modelState.rawCalls[0], "count(distinct posts.id) as aggregate"); }); - it('falls back to zero total when aggregate row is missing', async function () { - const {bookshelf} = createBookshelf({countRows: []}); + it("falls back to zero total when aggregate row is missing", async function () { + const { bookshelf } = createBookshelf({ countRows: [] }); const model = new bookshelf.Model(); - const result = await model.fetchPage({page: 1, limit: 10}); + const result = await model.fetchPage({ page: 1, limit: 10 }); assert.equal(result.pagination.total, 0); assert.equal(result.pagination.pages, 1); }); - it('sets only prev for last page pagination metadata', async function () { - const {bookshelf} = createBookshelf({countRows: [{aggregate: 10}]}); + it("sets only prev for last page pagination metadata", async function () { + const { bookshelf } = createBookshelf({ countRows: [{ aggregate: 10 }] }); const model = new bookshelf.Model(); - const result = await model.fetchPage({page: 2, limit: 5}); + const result = await model.fetchPage({ page: 2, limit: 5 }); assert.deepEqual(result.pagination, { page: 2, limit: 5, pages: 2, total: 10, next: null, - prev: 1 + prev: 1, }); }); - it('wraps offset/limit DB errors as NotFoundError', async function () { - const {bookshelf} = createBookshelf({ - fetchError: {errno: 20} + it("wraps offset/limit DB errors as NotFoundError", async function () { + const { bookshelf } = createBookshelf({ + fetchError: { errno: 20 }, }); const model = new bookshelf.Model(); - await assert.rejects(async () => { - await model.fetchPage({page: 1, limit: 10}); - }, (err) => { - assert.equal(err instanceof errors.NotFoundError, true); - assert.equal(err.message, 'Page not found'); - return true; - }); + await assert.rejects( + async () => { + await model.fetchPage({ page: 1, limit: 10 }); + }, + (err) => { + assert.equal(err instanceof errors.NotFoundError, true); + assert.equal(err.message, "Page not found"); + return true; + }, + ); }); - it('wraps SQL syntax errors as BadRequestError', async function () { - const {bookshelf} = createBookshelf({ - selectError: {errno: 1054} + it("wraps SQL syntax errors as BadRequestError", async function () { + const { bookshelf } = createBookshelf({ + selectError: { errno: 1054 }, }); const model = new bookshelf.Model(); - await assert.rejects(async () => { - await model.fetchPage({page: 1, limit: 10}); - }, (err) => { - assert.equal(err instanceof errors.BadRequestError, true); - assert.equal(err.message, 'Could not understand request.'); - return true; - }); + await assert.rejects( + async () => { + await model.fetchPage({ page: 1, limit: 10 }); + }, + (err) => { + assert.equal(err instanceof errors.BadRequestError, true); + assert.equal(err.message, "Could not understand request."); + return true; + }, + ); }); - it('rethrows unknown errors unchanged', async function () { - const rootError = new Error('boom'); - const {bookshelf} = createBookshelf({ - selectError: rootError + it("rethrows unknown errors unchanged", async function () { + const rootError = new Error("boom"); + const { bookshelf } = createBookshelf({ + selectError: rootError, }); const model = new bookshelf.Model(); - await assert.rejects(async () => { - await model.fetchPage({page: 1, limit: 10}); - }, (err) => { - assert.equal(err, rootError); - return true; - }); + await assert.rejects( + async () => { + await model.fetchPage({ page: 1, limit: 10 }); + }, + (err) => { + assert.equal(err, rootError); + return true; + }, + ); }); - it('rethrows unknown fetch errors unchanged', async function () { - const fetchError = new Error('fetch failed'); - const {bookshelf} = createBookshelf({ - fetchError + it("rethrows unknown fetch errors unchanged", async function () { + const fetchError = new Error("fetch failed"); + const { bookshelf } = createBookshelf({ + fetchError, }); const model = new bookshelf.Model(); - await assert.rejects(async () => { - await model.fetchPage({page: 1, limit: 10}); - }, (err) => { - assert.equal(err, fetchError); - return true; - }); + await assert.rejects( + async () => { + await model.fetchPage({ page: 1, limit: 10 }); + }, + (err) => { + assert.equal(err, fetchError); + return true; + }, + ); }); }); diff --git a/packages/bookshelf-plugins/README.md b/packages/bookshelf-plugins/README.md index cad424e3e..58dbe6ff9 100644 --- a/packages/bookshelf-plugins/README.md +++ b/packages/bookshelf-plugins/README.md @@ -8,36 +8,30 @@ or `yarn add @tryghost/bookshelf-plugins` - ## Purpose Convenience package that composes and exposes Ghost's Bookshelf plugin set in one place. ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests +# Copyright & License - - -# Copyright & License - -Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/bookshelf-plugins/index.js b/packages/bookshelf-plugins/index.js index fa5200817..8b8955430 100644 --- a/packages/bookshelf-plugins/index.js +++ b/packages/bookshelf-plugins/index.js @@ -1 +1 @@ -module.exports = require('./lib/bookshelf-plugins'); +module.exports = require("./lib/bookshelf-plugins"); diff --git a/packages/bookshelf-plugins/lib/bookshelf-plugins.js b/packages/bookshelf-plugins/lib/bookshelf-plugins.js index 3e6d314d1..a8d06d570 100644 --- a/packages/bookshelf-plugins/lib/bookshelf-plugins.js +++ b/packages/bookshelf-plugins/lib/bookshelf-plugins.js @@ -1,12 +1,12 @@ module.exports = { - collision: require('@tryghost/bookshelf-collision'), - customQuery: require('@tryghost/bookshelf-custom-query'), - eagerLoad: require('@tryghost/bookshelf-eager-load'), - filter: require('@tryghost/bookshelf-filter'), - hasPosts: require('@tryghost/bookshelf-has-posts'), - includeCount: require('@tryghost/bookshelf-include-count'), - order: require('@tryghost/bookshelf-order'), - pagination: require('@tryghost/bookshelf-pagination'), - search: require('@tryghost/bookshelf-search'), - transactionEvents: require('@tryghost/bookshelf-transaction-events') + collision: require("@tryghost/bookshelf-collision"), + customQuery: require("@tryghost/bookshelf-custom-query"), + eagerLoad: require("@tryghost/bookshelf-eager-load"), + filter: require("@tryghost/bookshelf-filter"), + hasPosts: require("@tryghost/bookshelf-has-posts"), + includeCount: require("@tryghost/bookshelf-include-count"), + order: require("@tryghost/bookshelf-order"), + pagination: require("@tryghost/bookshelf-pagination"), + search: require("@tryghost/bookshelf-search"), + transactionEvents: require("@tryghost/bookshelf-transaction-events"), }; diff --git a/packages/bookshelf-plugins/package.json b/packages/bookshelf-plugins/package.json index 9dc759d09..c2fa3d617 100644 --- a/packages/bookshelf-plugins/package.json +++ b/packages/bookshelf-plugins/package.json @@ -1,30 +1,27 @@ { "name": "@tryghost/bookshelf-plugins", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/bookshelf-plugins" }, - "author": "Ghost Foundation", - "license": "MIT", - "main": "index.js", - "scripts": { - "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" - }, "files": [ "index.js", "lib" ], + "main": "index.js", "publishConfig": { "access": "public" }, - "devDependencies": { - "sinon": "21.0.3" + "scripts": { + "dev": "echo \"Implement me!\"", + "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "dependencies": { "@tryghost/bookshelf-collision": "^2.0.3", @@ -37,5 +34,8 @@ "@tryghost/bookshelf-pagination": "^2.0.3", "@tryghost/bookshelf-search": "^2.0.3", "@tryghost/bookshelf-transaction-events": "^2.0.3" + }, + "devDependencies": { + "sinon": "21.0.3" } } diff --git a/packages/bookshelf-plugins/test/.eslintrc.js b/packages/bookshelf-plugins/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/bookshelf-plugins/test/.eslintrc.js +++ b/packages/bookshelf-plugins/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/bookshelf-plugins/test/bookshelf-plugins.test.js b/packages/bookshelf-plugins/test/bookshelf-plugins.test.js index 97a630d1f..cd92e6b19 100644 --- a/packages/bookshelf-plugins/test/bookshelf-plugins.test.js +++ b/packages/bookshelf-plugins/test/bookshelf-plugins.test.js @@ -1,25 +1,25 @@ -const assert = require('node:assert/strict'); -const plugins = require('..'); +const assert = require("node:assert/strict"); +const plugins = require(".."); -describe('@tryghost/bookshelf-plugins', function () { - it('exports all expected plugin modules', function () { +describe("@tryghost/bookshelf-plugins", function () { + it("exports all expected plugin modules", function () { assert.deepEqual(Object.keys(plugins).sort(), [ - 'collision', - 'customQuery', - 'eagerLoad', - 'filter', - 'hasPosts', - 'includeCount', - 'order', - 'pagination', - 'search', - 'transactionEvents' + "collision", + "customQuery", + "eagerLoad", + "filter", + "hasPosts", + "includeCount", + "order", + "pagination", + "search", + "transactionEvents", ]); }); - it('exports plugin functions', function () { + it("exports plugin functions", function () { for (const key of Object.keys(plugins)) { - assert.equal(typeof plugins[key], 'function'); + assert.equal(typeof plugins[key], "function"); } }); }); diff --git a/packages/bookshelf-search/README.md b/packages/bookshelf-search/README.md index 76ae2a1a5..ab15cc0c3 100644 --- a/packages/bookshelf-search/README.md +++ b/packages/bookshelf-search/README.md @@ -8,36 +8,30 @@ or `yarn add @tryghost/bookshelf-search` - ## Purpose Bookshelf plugin that adds search query helpers for text-based filtering across model fields. ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests +# Copyright & License - - -# Copyright & License - -Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/bookshelf-search/index.js b/packages/bookshelf-search/index.js index 6fd8081e9..f67c978b1 100644 --- a/packages/bookshelf-search/index.js +++ b/packages/bookshelf-search/index.js @@ -1 +1 @@ -module.exports = require('./lib/bookshelf-search'); +module.exports = require("./lib/bookshelf-search"); diff --git a/packages/bookshelf-search/lib/bookshelf-search.js b/packages/bookshelf-search/lib/bookshelf-search.js index 201428629..b18cd948d 100644 --- a/packages/bookshelf-search/lib/bookshelf-search.js +++ b/packages/bookshelf-search/lib/bookshelf-search.js @@ -9,7 +9,7 @@ const searchPlugin = function searchPlugin(Bookshelf) { this.searchQuery(qb, options.search); }); } - } + }, }); Bookshelf.Model = Model; diff --git a/packages/bookshelf-search/package.json b/packages/bookshelf-search/package.json index 3cbcd077b..4fa81b5b9 100644 --- a/packages/bookshelf-search/package.json +++ b/packages/bookshelf-search/package.json @@ -1,14 +1,21 @@ { "name": "@tryghost/bookshelf-search", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/bookshelf-search" }, - "author": "Ghost Foundation", - "license": "MIT", + "files": [ + "index.js", + "lib" + ], "main": "index.js", + "publishConfig": { + "access": "public" + }, "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", @@ -16,13 +23,6 @@ "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, - "files": [ - "index.js", - "lib" - ], - "publishConfig": { - "access": "public" - }, "devDependencies": { "sinon": "21.0.3" } diff --git a/packages/bookshelf-search/test/.eslintrc.js b/packages/bookshelf-search/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/bookshelf-search/test/.eslintrc.js +++ b/packages/bookshelf-search/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/bookshelf-search/test/bookshelf-search.test.js b/packages/bookshelf-search/test/bookshelf-search.test.js index 4915056e6..96028836b 100644 --- a/packages/bookshelf-search/test/bookshelf-search.test.js +++ b/packages/bookshelf-search/test/bookshelf-search.test.js @@ -1,8 +1,8 @@ -const assert = require('node:assert/strict'); -const sinon = require('sinon'); -const installPlugin = require('..'); +const assert = require("node:assert/strict"); +const sinon = require("sinon"); +const installPlugin = require(".."); -describe('@tryghost/bookshelf-search', function () { +describe("@tryghost/bookshelf-search", function () { let Bookshelf; let ParentModel; @@ -17,7 +17,7 @@ describe('@tryghost/bookshelf-search', function () { return Child; }; - Bookshelf = {Model: ParentModel}; + Bookshelf = { Model: ParentModel }; installPlugin(Bookshelf); }); @@ -25,34 +25,34 @@ describe('@tryghost/bookshelf-search', function () { sinon.restore(); }); - it('exports plugin from index', function () { - assert.equal(typeof require('../index'), 'function'); + it("exports plugin from index", function () { + assert.equal(typeof require("../index"), "function"); }); - it('replaces Bookshelf.Model with extended model', function () { + it("replaces Bookshelf.Model with extended model", function () { assert.notEqual(Bookshelf.Model, ParentModel); }); - it('provides default searchQuery function', function () { + it("provides default searchQuery function", function () { const model = new Bookshelf.Model(); - assert.equal(typeof model.searchQuery, 'function'); + assert.equal(typeof model.searchQuery, "function"); assert.equal(model.searchQuery(), undefined); }); - it('applySearchQuery calls query and forwards qb/search to searchQuery', function () { + it("applySearchQuery calls query and forwards qb/search to searchQuery", function () { const model = new Bookshelf.Model(); const qb = {}; model.searchQuery = sinon.stub(); - model.query = sinon.stub().callsFake(fn => fn(qb)); + model.query = sinon.stub().callsFake((fn) => fn(qb)); - model.applySearchQuery({search: 'news'}); + model.applySearchQuery({ search: "news" }); assert.equal(model.query.calledOnce, true); - assert.equal(model.searchQuery.calledOnceWithExactly(qb, 'news'), true); + assert.equal(model.searchQuery.calledOnceWithExactly(qb, "news"), true); }); - it('applySearchQuery does nothing when search option is missing', function () { + it("applySearchQuery does nothing when search option is missing", function () { const model = new Bookshelf.Model(); model.query = sinon.stub(); diff --git a/packages/bookshelf-transaction-events/README.md b/packages/bookshelf-transaction-events/README.md index 170ddaf31..eb0b78947 100644 --- a/packages/bookshelf-transaction-events/README.md +++ b/packages/bookshelf-transaction-events/README.md @@ -8,36 +8,30 @@ or `yarn add @tryghost/bookshelf-transaction-events` - ## Purpose Bookshelf plugin that emits lifecycle events scoped to database transaction boundaries. ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests +# Copyright & License - - -# Copyright & License - -Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/bookshelf-transaction-events/index.js b/packages/bookshelf-transaction-events/index.js index 5e4e8a635..7cac9ab00 100644 --- a/packages/bookshelf-transaction-events/index.js +++ b/packages/bookshelf-transaction-events/index.js @@ -1 +1 @@ -module.exports = require('./lib/bookshelf-transaction-events'); +module.exports = require("./lib/bookshelf-transaction-events"); diff --git a/packages/bookshelf-transaction-events/lib/bookshelf-transaction-events.js b/packages/bookshelf-transaction-events/lib/bookshelf-transaction-events.js index 6da2927ba..68d7d6ee5 100644 --- a/packages/bookshelf-transaction-events/lib/bookshelf-transaction-events.js +++ b/packages/bookshelf-transaction-events/lib/bookshelf-transaction-events.js @@ -12,13 +12,13 @@ module.exports = function (bookshelf) { t.commit = async function () { const originalReturn = await originalCommit.apply(t, arguments); - t.emit('committed', true); + t.emit("committed", true); return originalReturn; }; t.rollback = async function () { const originalReturn = await originalRollback.apply(t, arguments); - t.emit('committed', false); + t.emit("committed", false); return originalReturn; }; diff --git a/packages/bookshelf-transaction-events/package.json b/packages/bookshelf-transaction-events/package.json index f006b0f71..1e51cdcd7 100644 --- a/packages/bookshelf-transaction-events/package.json +++ b/packages/bookshelf-transaction-events/package.json @@ -1,14 +1,21 @@ { "name": "@tryghost/bookshelf-transaction-events", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/bookshelf-transaction-events" }, - "author": "Ghost Foundation", - "license": "MIT", + "files": [ + "index.js", + "lib" + ], "main": "index.js", + "publishConfig": { + "access": "public" + }, "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", @@ -16,13 +23,6 @@ "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, - "files": [ - "index.js", - "lib" - ], - "publishConfig": { - "access": "public" - }, "devDependencies": { "sinon": "21.0.3" } diff --git a/packages/bookshelf-transaction-events/test/.eslintrc.js b/packages/bookshelf-transaction-events/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/bookshelf-transaction-events/test/.eslintrc.js +++ b/packages/bookshelf-transaction-events/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/bookshelf-transaction-events/test/bookshelf-transaction-events.test.js b/packages/bookshelf-transaction-events/test/bookshelf-transaction-events.test.js index 057ea9b23..07127c794 100644 --- a/packages/bookshelf-transaction-events/test/bookshelf-transaction-events.test.js +++ b/packages/bookshelf-transaction-events/test/bookshelf-transaction-events.test.js @@ -1,8 +1,8 @@ -const assert = require('node:assert/strict'); -const sinon = require('sinon'); -const transactionEvents = require('../index'); +const assert = require("node:assert/strict"); +const sinon = require("sinon"); +const transactionEvents = require("../index"); -describe('@tryghost/bookshelf-transaction-events', function () { +describe("@tryghost/bookshelf-transaction-events", function () { let bookshelf; let transactionState; @@ -14,14 +14,14 @@ describe('@tryghost/bookshelf-transaction-events', function () { transactionState.boundThis = this; const trx = { emit: sinon.stub(), - commit: sinon.stub().resolves('COMMIT_RETURN'), - rollback: sinon.stub().resolves('ROLLBACK_RETURN') + commit: sinon.stub().resolves("COMMIT_RETURN"), + rollback: sinon.stub().resolves("ROLLBACK_RETURN"), }; transactionState.trx = trx; transactionState.originalCommit = trx.commit; transactionState.originalRollback = trx.rollback; return callback(trx); - } + }, }; }); @@ -29,44 +29,44 @@ describe('@tryghost/bookshelf-transaction-events', function () { sinon.restore(); }); - it('exports plugin from index', function () { - assert.equal(typeof require('../index'), 'function'); + it("exports plugin from index", function () { + assert.equal(typeof require("../index"), "function"); }); - it('patches transaction and emits committed=true after commit resolves', async function () { + it("patches transaction and emits committed=true after commit resolves", async function () { transactionEvents(bookshelf); const result = await bookshelf.transaction(async function (trx) { - return trx.commit('arg'); + return trx.commit("arg"); }); - assert.equal(result, 'COMMIT_RETURN'); + assert.equal(result, "COMMIT_RETURN"); assert.equal(transactionState.boundThis, bookshelf); - assert.equal(transactionState.originalCommit.calledOnceWithExactly('arg'), true); - assert.equal(transactionState.trx.emit.calledOnceWithExactly('committed', true), true); + assert.equal(transactionState.originalCommit.calledOnceWithExactly("arg"), true); + assert.equal(transactionState.trx.emit.calledOnceWithExactly("committed", true), true); sinon.assert.callOrder(transactionState.originalCommit, transactionState.trx.emit); }); - it('emits committed=false after rollback resolves', async function () { + it("emits committed=false after rollback resolves", async function () { transactionEvents(bookshelf); const result = await bookshelf.transaction(async function (trx) { - return trx.rollback('reason'); + return trx.rollback("reason"); }); - assert.equal(result, 'ROLLBACK_RETURN'); - assert.equal(transactionState.originalRollback.calledOnceWithExactly('reason'), true); - assert.equal(transactionState.trx.emit.calledOnceWithExactly('committed', false), true); + assert.equal(result, "ROLLBACK_RETURN"); + assert.equal(transactionState.originalRollback.calledOnceWithExactly("reason"), true); + assert.equal(transactionState.trx.emit.calledOnceWithExactly("committed", false), true); sinon.assert.callOrder(transactionState.originalRollback, transactionState.trx.emit); }); - it('returns callback return value', async function () { + it("returns callback return value", async function () { transactionEvents(bookshelf); const value = await bookshelf.transaction(async function () { - return 'CALLBACK_VALUE'; + return "CALLBACK_VALUE"; }); - assert.equal(value, 'CALLBACK_VALUE'); + assert.equal(value, "CALLBACK_VALUE"); }); }); diff --git a/packages/config/README.md b/packages/config/README.md index 3aa4bdc7d..c41b80c2b 100644 --- a/packages/config/README.md +++ b/packages/config/README.md @@ -8,36 +8,30 @@ or `yarn add @tryghost/config` - ## Purpose Configuration loader built on `nconf` that merges env vars, argv, defaults, and environment config files from the process root. ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests +# Copyright & License - - -# Copyright & License - -Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/config/index.js b/packages/config/index.js index 70df6b8c0..adea2fc50 100644 --- a/packages/config/index.js +++ b/packages/config/index.js @@ -1 +1 @@ -module.exports = require('./lib/config'); +module.exports = require("./lib/config"); diff --git a/packages/config/lib/config.js b/packages/config/lib/config.js index ceeb6e705..7028309ea 100644 --- a/packages/config/lib/config.js +++ b/packages/config/lib/config.js @@ -1,4 +1,4 @@ -const getConfig = require('./get-config'); +const getConfig = require("./get-config"); let config; function initConfig() { diff --git a/packages/config/lib/get-config.js b/packages/config/lib/get-config.js index c2c3ba261..4e62fa913 100644 --- a/packages/config/lib/get-config.js +++ b/packages/config/lib/get-config.js @@ -1,9 +1,9 @@ -const nconf = require('nconf'); -const fs = require('fs'); -const path = require('path'); -const rootUtils = require('@tryghost/root-utils'); +const nconf = require("nconf"); +const fs = require("fs"); +const path = require("path"); +const rootUtils = require("@tryghost/root-utils"); -const env = process.env.NODE_ENV || 'development'; +const env = process.env.NODE_ENV || "development"; module.exports = function getConfig() { const defaults = {}; @@ -11,21 +11,22 @@ module.exports = function getConfig() { const config = new nconf.Provider(); - if (parentPath && fs.existsSync(path.join(parentPath, 'config.example.json'))) { - Object.assign(defaults, require(path.join(parentPath, 'config.example.json'))); + if (parentPath && fs.existsSync(path.join(parentPath, "config.example.json"))) { + Object.assign(defaults, require(path.join(parentPath, "config.example.json"))); } - config.argv() + config + .argv() .env({ - separator: '__' + separator: "__", }) .file({ - file: path.join(parentPath, 'config.' + env + '.json') + file: path.join(parentPath, "config." + env + ".json"), }); - config.set('env', env); + config.set("env", env); config.defaults(defaults); return config; -}; \ No newline at end of file +}; diff --git a/packages/config/package.json b/packages/config/package.json index 7e9856ec2..7537247f4 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,14 +1,21 @@ { "name": "@tryghost/config", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/config" }, - "author": "Ghost Foundation", - "license": "MIT", + "files": [ + "index.js", + "lib" + ], "main": "index.js", + "publishConfig": { + "access": "public" + }, "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", @@ -16,13 +23,6 @@ "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, - "files": [ - "index.js", - "lib" - ], - "publishConfig": { - "access": "public" - }, "dependencies": { "@tryghost/root-utils": "^2.0.3", "nconf": "0.13.0" diff --git a/packages/config/test/.eslintrc.js b/packages/config/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/config/test/.eslintrc.js +++ b/packages/config/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/config/test/config.test.js b/packages/config/test/config.test.js index 174fab166..c725acd8d 100644 --- a/packages/config/test/config.test.js +++ b/packages/config/test/config.test.js @@ -1,15 +1,15 @@ -const assert = require('assert/strict'); -const {join} = require('path'); +const assert = require("assert/strict"); +const { join } = require("path"); -const rootUtils = require('@tryghost/root-utils'); +const rootUtils = require("@tryghost/root-utils"); const realRoot = rootUtils.getProcessRoot(); -const getConfigPath = require.resolve('../lib/get-config'); -const configPath = require.resolve('../lib/config'); -const indexPath = require.resolve('../index'); +const getConfigPath = require.resolve("../lib/get-config"); +const configPath = require.resolve("../lib/config"); +const indexPath = require.resolve("../index"); function fixturePath(path) { - return join(realRoot, '/test/fixtures', path || ''); + return join(realRoot, "/test/fixtures", path || ""); } function withRoot(root, fn) { @@ -43,36 +43,41 @@ function withEnv(nodeEnv, fn) { function loadFreshGetConfig() { delete require.cache[getConfigPath]; - return require('../lib/get-config'); + return require("../lib/get-config"); } -describe('Config', function () { - it('Empty config when no configuration file found', function () { - const config = withEnv('testing', () => withRoot('/tmp', () => loadFreshGetConfig()())); +describe("Config", function () { + it("Empty config when no configuration file found", function () { + const config = withEnv("testing", () => withRoot("/tmp", () => loadFreshGetConfig()())); - assert.equal(config.get('test'), undefined); - assert.equal(config.get('should-be-used'), undefined); - assert.equal(config.get('env'), 'testing'); + assert.equal(config.get("test"), undefined); + assert.equal(config.get("should-be-used"), undefined); + assert.equal(config.get("env"), "testing"); }); - it('Reads configuration file when exists', function () { - const config = withEnv('testing', () => withRoot(fixturePath(), () => loadFreshGetConfig()())); - - assert.equal(config.stores.file.file.endsWith('config/test/fixtures/config.testing.json'), true); - assert.equal(config.get('hello'), 'world'); - assert.equal(config.get('test'), 'root-config'); - assert.equal(config.get('should-be-used'), true); - assert.equal(config.get('env'), 'testing'); + it("Reads configuration file when exists", function () { + const config = withEnv("testing", () => + withRoot(fixturePath(), () => loadFreshGetConfig()()), + ); + + assert.equal( + config.stores.file.file.endsWith("config/test/fixtures/config.testing.json"), + true, + ); + assert.equal(config.get("hello"), "world"); + assert.equal(config.get("test"), "root-config"); + assert.equal(config.get("should-be-used"), true); + assert.equal(config.get("env"), "testing"); }); - it('defaults env to development when NODE_ENV is not set', function () { - const config = withEnv(undefined, () => withRoot('/tmp', () => loadFreshGetConfig()())); - assert.equal(config.get('env'), 'development'); + it("defaults env to development when NODE_ENV is not set", function () { + const config = withEnv(undefined, () => withRoot("/tmp", () => loadFreshGetConfig()())); + assert.equal(config.get("env"), "development"); }); - it('index exports lib/config and only initializes config once', function () { + it("index exports lib/config and only initializes config once", function () { const originalGetConfig = require(getConfigPath); - const fakeConfig = {name: 'fake-config'}; + const fakeConfig = { name: "fake-config" }; let callCount = 0; try { @@ -84,8 +89,8 @@ describe('Config', function () { delete require.cache[configPath]; delete require.cache[indexPath]; - const configFromLib = require('../lib/config'); - const configFromIndex = require('../'); + const configFromLib = require("../lib/config"); + const configFromIndex = require("../"); assert.equal(configFromLib, fakeConfig); assert.equal(configFromIndex, fakeConfig); diff --git a/packages/database-info/README.md b/packages/database-info/README.md index 845cb85b2..dd6b7605f 100644 --- a/packages/database-info/README.md +++ b/packages/database-info/README.md @@ -12,36 +12,30 @@ or `yarn add @tryghost/database-info` - ## Purpose Utility for detecting database driver, engine family, and version from a Knex connection. ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests +# Copyright & License - - -# Copyright & License - -Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/database-info/index.js b/packages/database-info/index.js index a3d5263f7..9898a3912 100644 --- a/packages/database-info/index.js +++ b/packages/database-info/index.js @@ -1 +1 @@ -module.exports = require('./lib/DatabaseInfo'); +module.exports = require("./lib/DatabaseInfo"); diff --git a/packages/database-info/lib/DatabaseInfo.js b/packages/database-info/lib/DatabaseInfo.js index 12e4d7b6f..9c471670b 100644 --- a/packages/database-info/lib/DatabaseInfo.js +++ b/packages/database-info/lib/DatabaseInfo.js @@ -13,54 +13,54 @@ module.exports = class DatabaseInfo { driver: this._driver, // A capitalized version of the specific database used - database: 'unknown', + database: "unknown", // A slugified version of the `database` - engine: 'unknown', + engine: "unknown", // The version of the database used - version: 'unknown' + version: "unknown", }; } async init() { switch (this._driver) { - case 'sqlite3': - this._databaseDetails.database = 'SQLite'; - this._databaseDetails.engine = 'sqlite3'; - this._databaseDetails.version = this._client.driver.VERSION; - break; - case 'mysql': - case 'mysql2': - try { - const version = await this._knex.raw('SELECT version() as version;'); - const mysqlVersion = version[0][0].version; - - if (mysqlVersion.includes('MariaDB')) { - this._databaseDetails.database = 'MariaDB'; - this._databaseDetails.engine = 'mariadb'; - this._databaseDetails.version = mysqlVersion.split('-')[0]; - } else { - this._databaseDetails.database = 'MySQL'; - - if (mysqlVersion.startsWith('5')) { - this._databaseDetails.engine = 'mysql5'; - } else if (mysqlVersion.startsWith('8')) { - this._databaseDetails.engine = 'mysql8'; + case "sqlite3": + this._databaseDetails.database = "SQLite"; + this._databaseDetails.engine = "sqlite3"; + this._databaseDetails.version = this._client.driver.VERSION; + break; + case "mysql": + case "mysql2": + try { + const version = await this._knex.raw("SELECT version() as version;"); + const mysqlVersion = version[0][0].version; + + if (mysqlVersion.includes("MariaDB")) { + this._databaseDetails.database = "MariaDB"; + this._databaseDetails.engine = "mariadb"; + this._databaseDetails.version = mysqlVersion.split("-")[0]; } else { - this._databaseDetails.engine = 'mysql'; - } + this._databaseDetails.database = "MySQL"; + + if (mysqlVersion.startsWith("5")) { + this._databaseDetails.engine = "mysql5"; + } else if (mysqlVersion.startsWith("8")) { + this._databaseDetails.engine = "mysql8"; + } else { + this._databaseDetails.engine = "mysql"; + } - this._databaseDetails.version = mysqlVersion; + this._databaseDetails.version = mysqlVersion; + } + } catch (err) { + return this._databaseDetails; } - } catch (err) { - return this._databaseDetails; - } - break; - default: - // This driver isn't supported so we should just leave the return - // object alone with the "unknown" strings - break; + break; + default: + // This driver isn't supported so we should just leave the return + // object alone with the "unknown" strings + break; } return this._databaseDetails; @@ -89,7 +89,7 @@ module.exports = class DatabaseInfo { */ static isSQLite(knex) { const driver = knex.client.config.client; - return ['sqlite3', 'better-sqlite3'].includes(driver); + return ["sqlite3", "better-sqlite3"].includes(driver); } /** @@ -98,7 +98,7 @@ module.exports = class DatabaseInfo { * @param {object} config */ static isSQLiteConfig(config) { - return ['sqlite3', 'better-sqlite3'].includes(config.client); + return ["sqlite3", "better-sqlite3"].includes(config.client); } /** @@ -108,7 +108,7 @@ module.exports = class DatabaseInfo { */ static isMySQL(knex) { const driver = knex.client.config.client; - return ['mysql', 'mysql2'].includes(driver); + return ["mysql", "mysql2"].includes(driver); } /** @@ -117,6 +117,6 @@ module.exports = class DatabaseInfo { * @param {object} config */ static isMySQLConfig(config) { - return ['mysql', 'mysql2'].includes(config.client); + return ["mysql", "mysql2"].includes(config.client); } }; diff --git a/packages/database-info/package.json b/packages/database-info/package.json index e8e7804ab..3347858f2 100644 --- a/packages/database-info/package.json +++ b/packages/database-info/package.json @@ -1,14 +1,21 @@ { "name": "@tryghost/database-info", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/database-info" }, - "author": "Ghost Foundation", - "license": "MIT", + "files": [ + "index.js", + "lib" + ], "main": "index.js", + "publishConfig": { + "access": "public" + }, "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", @@ -16,13 +23,6 @@ "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, - "files": [ - "index.js", - "lib" - ], - "publishConfig": { - "access": "public" - }, "devDependencies": { "knex": "3.1.0" } diff --git a/packages/database-info/test/.eslintrc.js b/packages/database-info/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/database-info/test/.eslintrc.js +++ b/packages/database-info/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/database-info/test/DatabaseInfo.test.js b/packages/database-info/test/DatabaseInfo.test.js index b4a9265d9..214011902 100644 --- a/packages/database-info/test/DatabaseInfo.test.js +++ b/packages/database-info/test/DatabaseInfo.test.js @@ -1,158 +1,158 @@ -const assert = require('assert/strict'); +const assert = require("assert/strict"); -const DatabaseInfo = require('../'); +const DatabaseInfo = require("../"); -function buildKnex({client, version = '0.0.0', rawResult, rawReject}) { +function buildKnex({ client, version = "0.0.0", rawResult, rawReject }) { return { client: { - config: {client}, - driver: {VERSION: version} + config: { client }, + driver: { VERSION: version }, }, raw: async () => { if (rawReject) { throw rawReject; } return rawResult; - } + }, }; } -describe('DatabaseInfo', function () { - it('should export a class', function () { - assert.equal(typeof DatabaseInfo, 'function'); +describe("DatabaseInfo", function () { + it("should export a class", function () { + assert.equal(typeof DatabaseInfo, "function"); }); - it('can construct the class', function () { - const knex = buildKnex({client: 'sqlite3'}); + it("can construct the class", function () { + const knex = buildKnex({ client: "sqlite3" }); const databaseInfo = new DatabaseInfo(knex); assert.ok(databaseInfo instanceof DatabaseInfo); }); - it('init recognises sqlite3 and sets sqlite details', async function () { - const knex = buildKnex({client: 'sqlite3', version: '3.45.0'}); + it("init recognises sqlite3 and sets sqlite details", async function () { + const knex = buildKnex({ client: "sqlite3", version: "3.45.0" }); const databaseInfo = new DatabaseInfo(knex); const details = await databaseInfo.init(); assert.deepEqual(details, { - driver: 'sqlite3', - database: 'SQLite', - engine: 'sqlite3', - version: '3.45.0' + driver: "sqlite3", + database: "SQLite", + engine: "sqlite3", + version: "3.45.0", }); - assert.equal(databaseInfo.getDriver(), 'sqlite3'); - assert.equal(databaseInfo.getDatabase(), 'SQLite'); - assert.equal(databaseInfo.getEngine(), 'sqlite3'); - assert.equal(databaseInfo.getVersion(), '3.45.0'); + assert.equal(databaseInfo.getDriver(), "sqlite3"); + assert.equal(databaseInfo.getDatabase(), "SQLite"); + assert.equal(databaseInfo.getEngine(), "sqlite3"); + assert.equal(databaseInfo.getVersion(), "3.45.0"); }); - it('init recognises mysql2 and maps MariaDB version', async function () { + it("init recognises mysql2 and maps MariaDB version", async function () { const knex = buildKnex({ - client: 'mysql2', - rawResult: [[{version: '10.6.18-MariaDB-1:10.6.18+maria~ubu2204'}]] + client: "mysql2", + rawResult: [[{ version: "10.6.18-MariaDB-1:10.6.18+maria~ubu2204" }]], }); const details = await new DatabaseInfo(knex).init(); assert.deepEqual(details, { - driver: 'mysql2', - database: 'MariaDB', - engine: 'mariadb', - version: '10.6.18' + driver: "mysql2", + database: "MariaDB", + engine: "mariadb", + version: "10.6.18", }); }); - it('init recognises mysql and maps MySQL 5 engine', async function () { + it("init recognises mysql and maps MySQL 5 engine", async function () { const knex = buildKnex({ - client: 'mysql', - rawResult: [[{version: '5.7.44'}]] + client: "mysql", + rawResult: [[{ version: "5.7.44" }]], }); const details = await new DatabaseInfo(knex).init(); assert.deepEqual(details, { - driver: 'mysql', - database: 'MySQL', - engine: 'mysql5', - version: '5.7.44' + driver: "mysql", + database: "MySQL", + engine: "mysql5", + version: "5.7.44", }); }); - it('init recognises mysql and maps MySQL 8 engine', async function () { + it("init recognises mysql and maps MySQL 8 engine", async function () { const knex = buildKnex({ - client: 'mysql', - rawResult: [[{version: '8.0.39'}]] + client: "mysql", + rawResult: [[{ version: "8.0.39" }]], }); const details = await new DatabaseInfo(knex).init(); assert.deepEqual(details, { - driver: 'mysql', - database: 'MySQL', - engine: 'mysql8', - version: '8.0.39' + driver: "mysql", + database: "MySQL", + engine: "mysql8", + version: "8.0.39", }); }); - it('init recognises mysql and maps unknown major to generic mysql engine', async function () { + it("init recognises mysql and maps unknown major to generic mysql engine", async function () { const knex = buildKnex({ - client: 'mysql', - rawResult: [[{version: '9.1.0'}]] + client: "mysql", + rawResult: [[{ version: "9.1.0" }]], }); const details = await new DatabaseInfo(knex).init(); assert.deepEqual(details, { - driver: 'mysql', - database: 'MySQL', - engine: 'mysql', - version: '9.1.0' + driver: "mysql", + database: "MySQL", + engine: "mysql", + version: "9.1.0", }); }); - it('init returns unknown details on mysql query failure', async function () { + it("init returns unknown details on mysql query failure", async function () { const knex = buildKnex({ - client: 'mysql2', - rawReject: new Error('cannot query version') + client: "mysql2", + rawReject: new Error("cannot query version"), }); const details = await new DatabaseInfo(knex).init(); assert.deepEqual(details, { - driver: 'mysql2', - database: 'unknown', - engine: 'unknown', - version: 'unknown' + driver: "mysql2", + database: "unknown", + engine: "unknown", + version: "unknown", }); }); - it('init keeps unknown details for unsupported drivers', async function () { - const knex = buildKnex({client: 'postgres'}); + it("init keeps unknown details for unsupported drivers", async function () { + const knex = buildKnex({ client: "postgres" }); const details = await new DatabaseInfo(knex).init(); assert.deepEqual(details, { - driver: 'postgres', - database: 'unknown', - engine: 'unknown', - version: 'unknown' + driver: "postgres", + database: "unknown", + engine: "unknown", + version: "unknown", }); }); - it('recognises sqlite drivers', function () { - assert.equal(DatabaseInfo.isSQLite(buildKnex({client: 'sqlite3'})), true); - assert.equal(DatabaseInfo.isSQLite(buildKnex({client: 'better-sqlite3'})), true); - assert.equal(DatabaseInfo.isSQLite(buildKnex({client: 'mysql'})), false); + it("recognises sqlite drivers", function () { + assert.equal(DatabaseInfo.isSQLite(buildKnex({ client: "sqlite3" })), true); + assert.equal(DatabaseInfo.isSQLite(buildKnex({ client: "better-sqlite3" })), true); + assert.equal(DatabaseInfo.isSQLite(buildKnex({ client: "mysql" })), false); }); - it('recognises mysql drivers', function () { - assert.equal(DatabaseInfo.isMySQL(buildKnex({client: 'mysql'})), true); - assert.equal(DatabaseInfo.isMySQL(buildKnex({client: 'mysql2'})), true); - assert.equal(DatabaseInfo.isMySQL(buildKnex({client: 'sqlite3'})), false); + it("recognises mysql drivers", function () { + assert.equal(DatabaseInfo.isMySQL(buildKnex({ client: "mysql" })), true); + assert.equal(DatabaseInfo.isMySQL(buildKnex({ client: "mysql2" })), true); + assert.equal(DatabaseInfo.isMySQL(buildKnex({ client: "sqlite3" })), false); }); - it('recognises sqlite config', function () { - assert.equal(DatabaseInfo.isSQLiteConfig({client: 'sqlite3'}), true); - assert.equal(DatabaseInfo.isSQLiteConfig({client: 'better-sqlite3'}), true); - assert.equal(DatabaseInfo.isSQLiteConfig({client: 'mysql'}), false); + it("recognises sqlite config", function () { + assert.equal(DatabaseInfo.isSQLiteConfig({ client: "sqlite3" }), true); + assert.equal(DatabaseInfo.isSQLiteConfig({ client: "better-sqlite3" }), true); + assert.equal(DatabaseInfo.isSQLiteConfig({ client: "mysql" }), false); }); - it('recognises mysql config', function () { - assert.equal(DatabaseInfo.isMySQLConfig({client: 'mysql'}), true); - assert.equal(DatabaseInfo.isMySQLConfig({client: 'mysql2'}), true); - assert.equal(DatabaseInfo.isMySQLConfig({client: 'sqlite3'}), false); + it("recognises mysql config", function () { + assert.equal(DatabaseInfo.isMySQLConfig({ client: "mysql" }), true); + assert.equal(DatabaseInfo.isMySQLConfig({ client: "mysql2" }), true); + assert.equal(DatabaseInfo.isMySQLConfig({ client: "sqlite3" }), false); }); }); diff --git a/packages/debug/README.md b/packages/debug/README.md index a4105764b..44887b42d 100644 --- a/packages/debug/README.md +++ b/packages/debug/README.md @@ -8,36 +8,30 @@ or `yarn add @tryghost/debug` - ## Purpose Ghost wrapper around the `debug` logger with shared namespacing conventions. ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests +# Copyright & License - - -# Copyright & License - -Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/debug/index.js b/packages/debug/index.js index 674f3a168..41b2aaf32 100644 --- a/packages/debug/index.js +++ b/packages/debug/index.js @@ -1 +1 @@ -module.exports = require('./lib/debug'); +module.exports = require("./lib/debug"); diff --git a/packages/debug/lib/debug.js b/packages/debug/lib/debug.js index cba74a633..9b5460b29 100644 --- a/packages/debug/lib/debug.js +++ b/packages/debug/lib/debug.js @@ -1,5 +1,5 @@ -const rootUtils = require('@tryghost/root-utils'); -const debug = require('debug'); +const rootUtils = require("@tryghost/root-utils"); +const debug = require("debug"); /** * @description Create a debug instance based on your package.json alias/name. @@ -14,7 +14,7 @@ module.exports = function initDebug(name) { let alias; try { - const pkg = require(parentPath + '/package.json'); + const pkg = require(parentPath + "/package.json"); if (pkg.alias) { alias = pkg.alias; @@ -22,10 +22,10 @@ module.exports = function initDebug(name) { alias = pkg.name; } } catch (err) { - alias = 'undefined'; + alias = "undefined"; } - return debug(alias + ':' + name); + return debug(alias + ":" + name); }; -module.exports._base = debug; \ No newline at end of file +module.exports._base = debug; diff --git a/packages/debug/package.json b/packages/debug/package.json index 81ad04a39..7cacb1d9d 100644 --- a/packages/debug/package.json +++ b/packages/debug/package.json @@ -1,14 +1,21 @@ { "name": "@tryghost/debug", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/debug" }, - "author": "Ghost Foundation", - "license": "MIT", + "files": [ + "index.js", + "lib" + ], "main": "index.js", + "publishConfig": { + "access": "public" + }, "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", @@ -16,13 +23,6 @@ "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, - "files": [ - "index.js", - "lib" - ], - "publishConfig": { - "access": "public" - }, "dependencies": { "@tryghost/root-utils": "^2.0.3", "debug": "4.4.3" diff --git a/packages/debug/test/.eslintrc.js b/packages/debug/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/debug/test/.eslintrc.js +++ b/packages/debug/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/debug/test/debug.test.js b/packages/debug/test/debug.test.js index 46319b284..03570d011 100644 --- a/packages/debug/test/debug.test.js +++ b/packages/debug/test/debug.test.js @@ -1,31 +1,31 @@ -const assert = require('assert/strict'); -const fs = require('fs'); -const os = require('os'); -const path = require('path'); +const assert = require("assert/strict"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); -const debugModulePath = require.resolve('../lib/debug'); -const rootUtilsPath = require.resolve('@tryghost/root-utils'); -const baseDebugPath = require.resolve('debug'); +const debugModulePath = require.resolve("../lib/debug"); +const rootUtilsPath = require.resolve("@tryghost/root-utils"); +const baseDebugPath = require.resolve("debug"); function createTempPackageJson(contents) { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'debug-pkg-')); - fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(contents), 'utf8'); + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "debug-pkg-")); + fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify(contents), "utf8"); return dir; } -function loadDebugWithStubs({callerRoot, baseDebugStub}) { +function loadDebugWithStubs({ callerRoot, baseDebugStub }) { const originalRootUtils = require(rootUtilsPath); const originalBaseDebug = require(baseDebugPath); require.cache[rootUtilsPath].exports = { ...originalRootUtils, - getCallerRoot: () => callerRoot + getCallerRoot: () => callerRoot, }; require.cache[baseDebugPath].exports = baseDebugStub; delete require.cache[debugModulePath]; - const initDebug = require('../'); - const lib = require('../lib/debug'); + const initDebug = require("../"); + const lib = require("../lib/debug"); return { initDebug, @@ -34,72 +34,75 @@ function loadDebugWithStubs({callerRoot, baseDebugStub}) { require.cache[rootUtilsPath].exports = originalRootUtils; require.cache[baseDebugPath].exports = originalBaseDebug; delete require.cache[debugModulePath]; - delete require.cache[require.resolve('../index')]; - } + delete require.cache[require.resolve("../index")]; + }, }; } -describe('debug', function () { - it('uses package alias when present', function () { - const callerRoot = createTempPackageJson({name: 'pkg-name', alias: '@alias/pkg'}); +describe("debug", function () { + it("uses package alias when present", function () { + const callerRoot = createTempPackageJson({ name: "pkg-name", alias: "@alias/pkg" }); let receivedNamespace; const baseDebugStub = (namespace) => { receivedNamespace = namespace; - return {namespace}; + return { namespace }; }; - const {initDebug, restore} = loadDebugWithStubs({callerRoot, baseDebugStub}); + const { initDebug, restore } = loadDebugWithStubs({ callerRoot, baseDebugStub }); try { - const instance = initDebug('test'); - assert.equal(instance.namespace, '@alias/pkg:test'); - assert.equal(receivedNamespace, '@alias/pkg:test'); + const instance = initDebug("test"); + assert.equal(instance.namespace, "@alias/pkg:test"); + assert.equal(receivedNamespace, "@alias/pkg:test"); } finally { restore(); - fs.rmSync(callerRoot, {recursive: true, force: true}); + fs.rmSync(callerRoot, { recursive: true, force: true }); } }); - it('uses package name when alias is missing', function () { - const callerRoot = createTempPackageJson({name: '@tryghost/debug'}); - const baseDebugStub = namespace => ({namespace}); + it("uses package name when alias is missing", function () { + const callerRoot = createTempPackageJson({ name: "@tryghost/debug" }); + const baseDebugStub = (namespace) => ({ namespace }); - const {initDebug, restore} = loadDebugWithStubs({callerRoot, baseDebugStub}); + const { initDebug, restore } = loadDebugWithStubs({ callerRoot, baseDebugStub }); try { - const instance = initDebug('test'); - assert.equal(instance.namespace, '@tryghost/debug:test'); + const instance = initDebug("test"); + assert.equal(instance.namespace, "@tryghost/debug:test"); } finally { restore(); - fs.rmSync(callerRoot, {recursive: true, force: true}); + fs.rmSync(callerRoot, { recursive: true, force: true }); } }); - it('falls back to undefined namespace when package.json cannot be loaded', function () { - const missingRoot = path.join(os.tmpdir(), 'missing-debug-root'); - const baseDebugStub = namespace => ({namespace}); + it("falls back to undefined namespace when package.json cannot be loaded", function () { + const missingRoot = path.join(os.tmpdir(), "missing-debug-root"); + const baseDebugStub = (namespace) => ({ namespace }); - const {initDebug, restore} = loadDebugWithStubs({callerRoot: missingRoot, baseDebugStub}); + const { initDebug, restore } = loadDebugWithStubs({ + callerRoot: missingRoot, + baseDebugStub, + }); try { - const instance = initDebug('test'); - assert.equal(instance.namespace, 'undefined:test'); + const instance = initDebug("test"); + assert.equal(instance.namespace, "undefined:test"); } finally { restore(); } }); - it('exposes the base debug module as _base', function () { - const callerRoot = createTempPackageJson({name: '@tryghost/debug'}); - const baseDebugStub = () => ({namespace: 'unused'}); + it("exposes the base debug module as _base", function () { + const callerRoot = createTempPackageJson({ name: "@tryghost/debug" }); + const baseDebugStub = () => ({ namespace: "unused" }); - const {lib, restore} = loadDebugWithStubs({callerRoot, baseDebugStub}); + const { lib, restore } = loadDebugWithStubs({ callerRoot, baseDebugStub }); try { assert.equal(lib._base, baseDebugStub); } finally { restore(); - fs.rmSync(callerRoot, {recursive: true, force: true}); + fs.rmSync(callerRoot, { recursive: true, force: true }); } }); }); diff --git a/packages/domain-events/README.md b/packages/domain-events/README.md index 668fd6cdb..ea0917443 100644 --- a/packages/domain-events/README.md +++ b/packages/domain-events/README.md @@ -7,13 +7,13 @@ Lightweight domain event bus for publishing and subscribing to application-level ## Usage ```js -const DomainEvents = require('@tryghost/domain-events'); +const DomainEvents = require("@tryghost/domain-events"); class MyEvent { constructor(message) { this.timestamp = new Date(); this.data = { - message + message, }; } } @@ -22,7 +22,7 @@ DomainEvents.subscribe(MyEvent, function handler(event) { console.log(event.data.message); }); -const event = new MyEvent('hello world'); +const event = new MyEvent("hello world"); DomainEvents.dispatch(event); ``` diff --git a/packages/domain-events/index.js b/packages/domain-events/index.js index e7a906a8a..f001a5d28 100644 --- a/packages/domain-events/index.js +++ b/packages/domain-events/index.js @@ -1 +1 @@ -module.exports = require('./lib/DomainEvents'); +module.exports = require("./lib/DomainEvents"); diff --git a/packages/domain-events/lib/DomainEvents.js b/packages/domain-events/lib/DomainEvents.js index 60adb3c5e..f71be74b2 100644 --- a/packages/domain-events/lib/DomainEvents.js +++ b/packages/domain-events/lib/DomainEvents.js @@ -1,5 +1,5 @@ -const EventEmitter = require('events').EventEmitter; -const logging = require('@tryghost/logging'); +const EventEmitter = require("events").EventEmitter; +const logging = require("@tryghost/logging"); /** * @template T @@ -18,7 +18,7 @@ class DomainEvents { * @private * @type EventEmitter */ - static ee = new EventEmitter; + static ee = new EventEmitter(); /** * @template Data @@ -33,7 +33,7 @@ class DomainEvents { try { await handler(event); } catch (e) { - logging.error('Unhandled error in event handler for event: ' + Event.name); + logging.error("Unhandled error in event handler for event: " + Event.name); logging.error(e); } if (this.#trackingEnabled) { @@ -69,7 +69,7 @@ class DomainEvents { static #awaitQueue = []; static #dispatchCount = 0; static #processedCount = 0; - static #trackingEnabled = process.env.NODE_ENV?.startsWith('test'); + static #trackingEnabled = process.env.NODE_ENV?.startsWith("test"); /** * Waits for all the events in the queue to be dispatched and fully processed (async). @@ -81,7 +81,7 @@ class DomainEvents { resolve(); return; } - this.#awaitQueue.push({resolve}); + this.#awaitQueue.push({ resolve }); }); } diff --git a/packages/domain-events/lib/index.d.ts b/packages/domain-events/lib/index.d.ts index b17c0aa55..43d1b9000 100644 --- a/packages/domain-events/lib/index.d.ts +++ b/packages/domain-events/lib/index.d.ts @@ -1 +1 @@ -export declare type ConstructorOf = new (...args: any[]) => T +export declare type ConstructorOf = new (...args: any[]) => T; diff --git a/packages/domain-events/package.json b/packages/domain-events/package.json index be94637fb..7494317bd 100644 --- a/packages/domain-events/package.json +++ b/packages/domain-events/package.json @@ -1,13 +1,22 @@ { "name": "@tryghost/domain-events", "version": "3.0.3", - "publishConfig": { - "access": "public" - }, - "author": "Ghost Foundation", "license": "MIT", + "author": "Ghost Foundation", + "repository": { + "type": "git", + "url": "git+https://github.com/TryGhost/framework.git", + "directory": "packages/domain-events" + }, + "files": [ + "index.js", + "lib" + ], "main": "index.js", "types": "types", + "publishConfig": { + "access": "public" + }, "scripts": { "dev": "echo \"Implement me!\"", "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", @@ -17,16 +26,7 @@ "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", "lint:eslint": "yarn lint:code && yarn lint:test" }, - "files": [ - "index.js", - "lib" - ], "devDependencies": { "@tryghost/logging": "^4.0.3" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/TryGhost/framework.git", - "directory": "packages/domain-events" } } diff --git a/packages/domain-events/test/.eslintrc.js b/packages/domain-events/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/domain-events/test/.eslintrc.js +++ b/packages/domain-events/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/domain-events/test/DomainEvents.test.js b/packages/domain-events/test/DomainEvents.test.js index a1adddcd2..d0cafe429 100644 --- a/packages/domain-events/test/DomainEvents.test.js +++ b/packages/domain-events/test/DomainEvents.test.js @@ -1,7 +1,7 @@ -const DomainEvents = require('../'); -const assert = require('assert/strict'); -const sinon = require('sinon'); -const logging = require('@tryghost/logging'); +const DomainEvents = require("../"); +const assert = require("assert/strict"); +const sinon = require("sinon"); +const logging = require("@tryghost/logging"); class TestEvent { /** @@ -10,23 +10,24 @@ class TestEvent { constructor(message) { this.timestamp = new Date(); this.data = { - message + message, }; } } -const sleep = ms => new Promise((resolve) => { - setTimeout(resolve, ms); -}); +const sleep = (ms) => + new Promise((resolve) => { + setTimeout(resolve, ms); + }); -describe('DomainEvents', function () { +describe("DomainEvents", function () { afterEach(function () { sinon.restore(); DomainEvents.ee.removeAllListeners(); }); - it('Will call multiple subscribers with the event when it is dispatched', async function () { - const event = new TestEvent('Hello, world!'); + it("Will call multiple subscribers with the event when it is dispatched", async function () { + const event = new TestEvent("Hello, world!"); let events = []; @@ -57,17 +58,17 @@ describe('DomainEvents', function () { assert.equal(events[1], event); }); - it('Catches async errors in handlers', async function () { - const event = new TestEvent('Hello, world!'); + it("Catches async errors in handlers", async function () { + const event = new TestEvent("Hello, world!"); - const stub = sinon.stub(logging, 'error').returns(); + const stub = sinon.stub(logging, "error").returns(); /** * @param {TestEvent} receivedEvent */ async function handler1() { await sleep(10); - throw new Error('Test error'); + throw new Error("Test error"); } DomainEvents.subscribe(TestEvent, handler1); @@ -77,13 +78,13 @@ describe('DomainEvents', function () { assert.equal(stub.calledTwice, true); }); - describe('allSettled', function () { - it('Resolves when there are no events', async function () { + describe("allSettled", function () { + it("Resolves when there are no events", async function () { await DomainEvents.allSettled(); assert(true); }); - it('Waits for all listeners', async function () { + it("Waits for all listeners", async function () { let counter = 0; DomainEvents.subscribe(TestEvent, async () => { await sleep(20); @@ -94,7 +95,7 @@ describe('DomainEvents', function () { counter += 1; }); - DomainEvents.dispatch(new TestEvent('Hello, world!')); + DomainEvents.dispatch(new TestEvent("Hello, world!")); await DomainEvents.allSettled(); assert.equal(counter, 2); }); diff --git a/packages/elasticsearch/README.md b/packages/elasticsearch/README.md index 0ad647afd..73de2ef9c 100644 --- a/packages/elasticsearch/README.md +++ b/packages/elasticsearch/README.md @@ -8,35 +8,30 @@ or `yarn add @tryghost/elasticsearch` - ## Purpose Elasticsearch client wrapper plus Bunyan stream integration used by Ghost logging/metrics flows. ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests - - -# Copyright & License +# Copyright & License Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/elasticsearch/index.js b/packages/elasticsearch/index.js index 749f2be82..568bf69ce 100644 --- a/packages/elasticsearch/index.js +++ b/packages/elasticsearch/index.js @@ -1,2 +1,2 @@ -module.exports = require('./lib/ElasticSearch'); -module.exports.BunyanStream = require('./lib/ElasticSearchBunyan'); +module.exports = require("./lib/ElasticSearch"); +module.exports.BunyanStream = require("./lib/ElasticSearchBunyan"); diff --git a/packages/elasticsearch/lib/ElasticSearch.js b/packages/elasticsearch/lib/ElasticSearch.js index a608313e4..b3ef91f8b 100644 --- a/packages/elasticsearch/lib/ElasticSearch.js +++ b/packages/elasticsearch/lib/ElasticSearch.js @@ -1,5 +1,5 @@ -const {Client} = require('@elastic/elasticsearch'); -const debug = require('@tryghost/debug')('logging:elasticsearch'); +const { Client } = require("@elastic/elasticsearch"); +const debug = require("@tryghost/debug")("logging:elasticsearch"); // Singleton client - multiple children made from it for a single connection pool let client; @@ -9,7 +9,7 @@ class ElasticSearch { if (!client) { client = new Client(clientConfig); } - + this.client = client.child(); } @@ -19,22 +19,22 @@ class ElasticSearch { * @param {Object | string} index Index - either string representing the index or a property bag containing the index and other parameters */ async index(data, index) { - if (typeof data !== 'object') { - debug('ElasticSearch transport requires log data to be an object'); + if (typeof data !== "object") { + debug("ElasticSearch transport requires log data to be an object"); return; } - if (typeof index === 'string') { - index = {index}; + if (typeof index === "string") { + index = { index }; } try { await this.client.index({ body: data, - ...index + ...index, }); } catch (error) { - debug('Failed to ship log', error.message); + debug("Failed to ship log", error.message); } } } diff --git a/packages/elasticsearch/lib/ElasticSearchBunyan.js b/packages/elasticsearch/lib/ElasticSearchBunyan.js index 722dcaf3a..c7083c8eb 100644 --- a/packages/elasticsearch/lib/ElasticSearchBunyan.js +++ b/packages/elasticsearch/lib/ElasticSearchBunyan.js @@ -1,6 +1,6 @@ -const ElasticSearch = require('./ElasticSearch'); -const {PassThrough} = require('stream'); -const split = require('split2'); +const ElasticSearch = require("./ElasticSearch"); +const { PassThrough } = require("stream"); +const split = require("split2"); // Create a writable stream which pipes data written into it, into the bulk helper @@ -19,10 +19,10 @@ class ElasticSearchBunyan { datasource: stream.pipe(split()), onDocument() { return { - create: {_index: index} + create: { _index: index }, }; }, - pipeline + pipeline, }); return stream; diff --git a/packages/elasticsearch/package.json b/packages/elasticsearch/package.json index e69610b27..cabb2f11f 100644 --- a/packages/elasticsearch/package.json +++ b/packages/elasticsearch/package.json @@ -1,34 +1,34 @@ { "name": "@tryghost/elasticsearch", "version": "5.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/elasticsearch" }, - "author": "Ghost Foundation", - "license": "MIT", - "main": "index.js", - "scripts": { - "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" - }, "files": [ "index.js", "lib" ], + "main": "index.js", "publishConfig": { "access": "public" }, - "devDependencies": { - "sinon": "21.0.3" + "scripts": { + "dev": "echo \"Implement me!\"", + "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "dependencies": { "@elastic/elasticsearch": "9.3.4", "@tryghost/debug": "^2.0.3", "split2": "4.2.0" + }, + "devDependencies": { + "sinon": "21.0.3" } } diff --git a/packages/elasticsearch/test/.eslintrc.js b/packages/elasticsearch/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/elasticsearch/test/.eslintrc.js +++ b/packages/elasticsearch/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/elasticsearch/test/ElasticSearch.test.js b/packages/elasticsearch/test/ElasticSearch.test.js index 2905192fd..5e89191ef 100644 --- a/packages/elasticsearch/test/ElasticSearch.test.js +++ b/packages/elasticsearch/test/ElasticSearch.test.js @@ -1,42 +1,42 @@ -const assert = require('assert/strict'); -const sinon = require('sinon'); +const assert = require("assert/strict"); +const sinon = require("sinon"); const sandbox = sinon.createSandbox(); -const {Client} = require('@elastic/elasticsearch'); -const ElasticSearch = require('../index'); -const ElasticSearchBunyan = require('../lib/ElasticSearchBunyan'); +const { Client } = require("@elastic/elasticsearch"); +const ElasticSearch = require("../index"); +const ElasticSearchBunyan = require("../lib/ElasticSearchBunyan"); const testClientConfig = { - node: 'http://test-elastic-client', + node: "http://test-elastic-client", auth: { - username: 'user', - password: 'pass' - } + username: "user", + password: "pass", + }, }; const indexConfig = { - index: 'test-index', - pipeline: 'test-pipeline' + index: "test-index", + pipeline: "test-pipeline", }; -describe('ElasticSearch', function () { +describe("ElasticSearch", function () { afterEach(function () { sandbox.restore(); }); - it('Processes client configuration', function () { + it("Processes client configuration", function () { const es = new ElasticSearch(testClientConfig); assert.ok(es.client); - assert.equal(typeof es.client.index, 'function'); + assert.equal(typeof es.client.index, "function"); }); - it('Processes index configuration', async function () { + it("Processes index configuration", async function () { const testBody = { - message: 'Test data!' + message: "Test data!", }; - const indexStub = sandbox.stub(Client.prototype, 'index').callsFake((data) => { + const indexStub = sandbox.stub(Client.prototype, "index").callsFake((data) => { assert.ok(data.body); assert.deepEqual(data.body, testBody); assert.equal(data.index, indexConfig.index); @@ -49,66 +49,76 @@ describe('ElasticSearch', function () { assert.equal(indexStub.called, true); }); - it('Calls index on valid events', async function () { + it("Calls index on valid events", async function () { const es = new ElasticSearch(testClientConfig); - const indexStub = sandbox.stub(Client.prototype, 'index'); + const indexStub = sandbox.stub(Client.prototype, "index"); - await es.index({ - message: 'test' - }, indexConfig); + await es.index( + { + message: "test", + }, + indexConfig, + ); assert.equal(indexStub.callCount, 1); }); - it('Does not index invalid events', async function () { + it("Does not index invalid events", async function () { const es = new ElasticSearch(testClientConfig); - const indexStub = sandbox.stub(Client.prototype, 'index'); + const indexStub = sandbox.stub(Client.prototype, "index"); - await es.index('not an object', indexConfig); + await es.index("not an object", indexConfig); assert.equal(indexStub.callCount, 0); }); - it('Uses index config as a string', async function () { + it("Uses index config as a string", async function () { const es = new ElasticSearch(testClientConfig); - const indexStub = sandbox.stub(Client.prototype, 'index').callsFake((data) => { + const indexStub = sandbox.stub(Client.prototype, "index").callsFake((data) => { assert.equal(data.index, indexConfig.index); }); - await es.index({ - message: 'Test data' - }, indexConfig.index); + await es.index( + { + message: "Test data", + }, + indexConfig.index, + ); assert.equal(indexStub.callCount, 1); }); - it('Catches index failures without throwing', async function () { + it("Catches index failures without throwing", async function () { const es = new ElasticSearch(testClientConfig); - sandbox.stub(Client.prototype, 'index').rejects(new Error('boom')); + sandbox.stub(Client.prototype, "index").rejects(new Error("boom")); await assert.doesNotReject(async () => { - await es.index({message: 'Test data'}, indexConfig); + await es.index({ message: "Test data" }, indexConfig); }); }); }); -describe('ElasticSearch Bunyan', function () { +describe("ElasticSearch Bunyan", function () { afterEach(function () { sandbox.restore(); }); - it('Can index using the Bunyan API', function () { - const es = new ElasticSearchBunyan(testClientConfig, indexConfig.index, indexConfig.pipeline); + it("Can index using the Bunyan API", function () { + const es = new ElasticSearchBunyan( + testClientConfig, + indexConfig.index, + indexConfig.pipeline, + ); - const bulkStub = sandbox.stub(es.client.client.helpers, 'bulk').callsFake((data) => { + const bulkStub = sandbox.stub(es.client.client.helpers, "bulk").callsFake((data) => { assert.equal(data.pipeline, indexConfig.pipeline); assert.deepEqual(data.onDocument(), { - create: {_index: indexConfig.index} + create: { _index: indexConfig.index }, }); }); const stream = es.getStream(); - stream.write(JSON.stringify({message: 'Test data'})); + stream.write(JSON.stringify({ message: "Test data" })); assert.equal(bulkStub.callCount, 1); }); diff --git a/packages/email-mock-receiver/README.md b/packages/email-mock-receiver/README.md index afc4b2727..80cb831d2 100644 --- a/packages/email-mock-receiver/README.md +++ b/packages/email-mock-receiver/README.md @@ -16,19 +16,16 @@ Test helper that captures outbound email payloads for assertions in automated te ## Usage - ## Develop This is a monorepo package. Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests - diff --git a/packages/email-mock-receiver/index.js b/packages/email-mock-receiver/index.js index d209eaf04..ea57f00e7 100644 --- a/packages/email-mock-receiver/index.js +++ b/packages/email-mock-receiver/index.js @@ -1 +1 @@ -module.exports = require('./lib/EmailMockReceiver'); +module.exports = require("./lib/EmailMockReceiver"); diff --git a/packages/email-mock-receiver/lib/EmailMockReceiver.js b/packages/email-mock-receiver/lib/EmailMockReceiver.js index 7535c2b9a..12edd92f4 100644 --- a/packages/email-mock-receiver/lib/EmailMockReceiver.js +++ b/packages/email-mock-receiver/lib/EmailMockReceiver.js @@ -1,12 +1,12 @@ -const assert = require('assert'); -const {AssertionError} = require('assert'); +const assert = require("assert"); +const { AssertionError } = require("assert"); class EmailMockReceiver { #sendResponse; #snapshotManager; #snapshots = []; - constructor({snapshotManager, sendResponse = 'Mail is disabled'}) { + constructor({ snapshotManager, sendResponse = "Mail is disabled" }) { this.#snapshotManager = snapshotManager; this.#sendResponse = sendResponse; } @@ -38,7 +38,7 @@ class EmailMockReceiver { * @returns {EmailMockReceiver} current instance */ assertSentEmailCount(count) { - assert.equal(this.#snapshots.length, count, 'Email count does not match'); + assert.equal(this.#snapshots.length, count, "Email count does not match"); return this; } @@ -56,12 +56,12 @@ class EmailMockReceiver { properties: null, field: field, hint: `[${field} ${snapshotIndex + 1}]`, - error + error, }; let text = this.#snapshots[snapshotIndex][field]; if (replacements.length) { - for (const [, {pattern, replacement}] of Object.entries(replacements)) { + for (const [, { pattern, replacement }] of Object.entries(replacements)) { text = text.replace(pattern, replacement); } } @@ -80,7 +80,7 @@ class EmailMockReceiver { * @returns {EmailMockReceiver} current instance */ matchHTMLSnapshot(replacements = [], snapshotIndex = 0) { - return this.#matchTextSnapshot(replacements, snapshotIndex, 'html'); + return this.#matchTextSnapshot(replacements, snapshotIndex, "html"); } /** @@ -90,7 +90,7 @@ class EmailMockReceiver { * @returns {EmailMockReceiver} current instance */ matchPlaintextSnapshot(replacements = [], snapshotIndex = 0) { - return this.#matchTextSnapshot(replacements, snapshotIndex, 'text'); + return this.#matchTextSnapshot(replacements, snapshotIndex, "text"); } /** @@ -103,18 +103,21 @@ class EmailMockReceiver { const error = new AssertionError({}); let assertion = { properties: properties, - field: 'metadata', + field: "metadata", hint: `[metadata ${snapshotIndex + 1}]`, - error + error, }; const metadata = Object.assign({}, this.#snapshots[snapshotIndex]); delete metadata.html; delete metadata.text; - this.#snapshotManager.assertSnapshot({ - metadata - }, assertion); + this.#snapshotManager.assertSnapshot( + { + metadata, + }, + assertion, + ); return this; } diff --git a/packages/email-mock-receiver/package.json b/packages/email-mock-receiver/package.json index 52b68f5c9..243cd1554 100644 --- a/packages/email-mock-receiver/package.json +++ b/packages/email-mock-receiver/package.json @@ -1,14 +1,21 @@ { "name": "@tryghost/email-mock-receiver", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/email-mock-receiver" }, - "author": "Ghost Foundation", - "license": "MIT", + "files": [ + "index.js", + "lib" + ], "main": "index.js", + "publishConfig": { + "access": "public" + }, "scripts": { "dev": "echo \"Implement me!\"", "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", @@ -18,13 +25,6 @@ "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", "lint:eslint": "yarn lint:code && yarn lint:test" }, - "files": [ - "index.js", - "lib" - ], - "publishConfig": { - "access": "public" - }, "devDependencies": { "sinon": "21.0.3" } diff --git a/packages/email-mock-receiver/test/.eslintrc.js b/packages/email-mock-receiver/test/.eslintrc.js index ef13dee0e..c9b01755e 100644 --- a/packages/email-mock-receiver/test/.eslintrc.js +++ b/packages/email-mock-receiver/test/.eslintrc.js @@ -1,9 +1,7 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ], + plugins: ["ghost"], + extends: ["plugin:ghost/test"], globals: { - beforeAll: 'readonly' - } + beforeAll: "readonly", + }, }; diff --git a/packages/email-mock-receiver/test/EmailMockReceiver.test.js b/packages/email-mock-receiver/test/EmailMockReceiver.test.js index d3f51bbd3..859be6ac8 100644 --- a/packages/email-mock-receiver/test/EmailMockReceiver.test.js +++ b/packages/email-mock-receiver/test/EmailMockReceiver.test.js @@ -1,20 +1,20 @@ -const assert = require('assert/strict'); -const sinon = require('sinon'); +const assert = require("assert/strict"); +const sinon = require("sinon"); -const EmailMockReceiver = require('../index'); +const EmailMockReceiver = require("../index"); -describe('Email mock receiver', function () { +describe("Email mock receiver", function () { let snapshotManager; let emailMockReceiver; beforeAll(function () { snapshotManager = { - assertSnapshot: sinon.spy() + assertSnapshot: sinon.spy(), }; }); beforeEach(function () { - emailMockReceiver = new EmailMockReceiver({snapshotManager}); + emailMockReceiver = new EmailMockReceiver({ snapshotManager }); }); afterEach(function () { @@ -22,17 +22,17 @@ describe('Email mock receiver', function () { emailMockReceiver.reset(); }); - it('Can initialize', function () { - assert.ok(new EmailMockReceiver({snapshotManager: {}})); + it("Can initialize", function () { + assert.ok(new EmailMockReceiver({ snapshotManager: {} })); }); - it('Can chain match snapshot methods', function () { + it("Can chain match snapshot methods", function () { emailMockReceiver.send({ - html: '
test
', - text: 'test text lorem ipsum', + html: "
test
", + text: "test text lorem ipsum", metadata: { - to: 'test@example.com' - } + to: "test@example.com", + }, }); emailMockReceiver @@ -43,230 +43,242 @@ describe('Email mock receiver', function () { assert.equal(snapshotManager.assertSnapshot.calledThrice, true); assert.deepEqual(snapshotManager.assertSnapshot.args[0][0], { - html: '
test
' + html: "
test
", }); assert.deepEqual(snapshotManager.assertSnapshot.args[1][0], { - text: 'test text lorem ipsum' + text: "test text lorem ipsum", }); assert.deepEqual(snapshotManager.assertSnapshot.args[2][0], { metadata: { metadata: { - to: 'test@example.com' - } - } + to: "test@example.com", + }, + }, }); }); - describe('matchHTMLSnapshot', function () { - it('Can match primitive HTML snapshot', function () { - emailMockReceiver.send({html: '
test
'}); + describe("matchHTMLSnapshot", function () { + it("Can match primitive HTML snapshot", function () { + emailMockReceiver.send({ html: "
test
" }); emailMockReceiver.matchHTMLSnapshot(); assert.equal(snapshotManager.assertSnapshot.calledOnce, true); assert.deepEqual(snapshotManager.assertSnapshot.args[0][0], { - html: '
test
' + html: "
test
", }); }); - it('Can match first HTML snapshot with multiple send requests are executed', function () { - emailMockReceiver.send({html: '
test 1
'}); - emailMockReceiver.send({html: '
test 2
'}); + it("Can match first HTML snapshot with multiple send requests are executed", function () { + emailMockReceiver.send({ html: "
test 1
" }); + emailMockReceiver.send({ html: "
test 2
" }); emailMockReceiver.matchHTMLSnapshot(); assert.equal(snapshotManager.assertSnapshot.calledOnce, true); assert.deepEqual(snapshotManager.assertSnapshot.args[0][0], { - html: '
test 1
' + html: "
test 1
", }); }); - it('Can match HTML snapshot with dynamic URL query parameters', function () { + it("Can match HTML snapshot with dynamic URL query parameters", function () { emailMockReceiver.send({ - html: '
test https://127.0.0.1:2369/welcome/?token=JRexE3uutntD6F6WXSVaDZke91fTjpvO&action=signup
' + html: "
test https://127.0.0.1:2369/welcome/?token=JRexE3uutntD6F6WXSVaDZke91fTjpvO&action=signup
", }); - emailMockReceiver.matchHTMLSnapshot([{ - pattern: /token=(\w+)/gmi, - replacement: 'token=TEST_TOKEN' - }]); + emailMockReceiver.matchHTMLSnapshot([ + { + pattern: /token=(\w+)/gim, + replacement: "token=TEST_TOKEN", + }, + ]); assert.equal(snapshotManager.assertSnapshot.calledOnce, true); assert.deepEqual(snapshotManager.assertSnapshot.args[0][0], { - html: '
test https://127.0.0.1:2369/welcome/?token=TEST_TOKEN&action=signup
' + html: "
test https://127.0.0.1:2369/welcome/?token=TEST_TOKEN&action=signup
", }); }); - it('Can match HTML snapshot with dynamic version in content', function () { + it("Can match HTML snapshot with dynamic version in content", function () { emailMockReceiver.send({ - html: '
this email contains a dynamic version string v5.45
' + html: "
this email contains a dynamic version string v5.45
", }); - emailMockReceiver.matchHTMLSnapshot([{ - pattern: /v\d+.\d+/gmi, - replacement: 'v5.0' - }]); + emailMockReceiver.matchHTMLSnapshot([ + { + pattern: /v\d+.\d+/gim, + replacement: "v5.0", + }, + ]); assert.equal(snapshotManager.assertSnapshot.calledOnce, true); assert.deepEqual(snapshotManager.assertSnapshot.args[0][0], { - html: '
this email contains a dynamic version string v5.0
' + html: "
this email contains a dynamic version string v5.0
", }); }); - it('Cant match HTML snapshot with multiple occurrences of dynamic content', function () { + it("Cant match HTML snapshot with multiple occurrences of dynamic content", function () { emailMockReceiver.send({ - html: '
this email contains a dynamic version string once v5.45 and twice v4.28
' + html: "
this email contains a dynamic version string once v5.45 and twice v4.28
", }); - emailMockReceiver.matchHTMLSnapshot([{ - pattern: /v\d+.\d+/gmi, - replacement: 'v5.0' - }]); + emailMockReceiver.matchHTMLSnapshot([ + { + pattern: /v\d+.\d+/gim, + replacement: "v5.0", + }, + ]); assert.equal(snapshotManager.assertSnapshot.calledOnce, true); assert.deepEqual(snapshotManager.assertSnapshot.args[0][0], { - html: '
this email contains a dynamic version string once v5.0 and twice v5.0
' + html: "
this email contains a dynamic version string once v5.0 and twice v5.0
", }); }); }); - describe('matchPlaintextSnapshot', function () { - it('Can match primitive text snapshot', function () { - emailMockReceiver.send({text: 'test text lorem ipsum'}); + describe("matchPlaintextSnapshot", function () { + it("Can match primitive text snapshot", function () { + emailMockReceiver.send({ text: "test text lorem ipsum" }); emailMockReceiver.matchPlaintextSnapshot(); assert.equal(snapshotManager.assertSnapshot.calledOnce, true); assert.deepEqual(snapshotManager.assertSnapshot.args[0][0], { - text: 'test text lorem ipsum' + text: "test text lorem ipsum", }); }); - it('Can match text snapshot with multiple send requests are executed', function () { - emailMockReceiver.send({text: 'test 1'}); - emailMockReceiver.send({text: 'test 2'}); + it("Can match text snapshot with multiple send requests are executed", function () { + emailMockReceiver.send({ text: "test 1" }); + emailMockReceiver.send({ text: "test 2" }); emailMockReceiver.matchPlaintextSnapshot(); assert.equal(snapshotManager.assertSnapshot.calledOnce, true); assert.deepEqual(snapshotManager.assertSnapshot.args[0][0], { - text: 'test 1' + text: "test 1", }); emailMockReceiver.matchPlaintextSnapshot([], 1); assert.equal(snapshotManager.assertSnapshot.calledTwice, true); assert.deepEqual(snapshotManager.assertSnapshot.args[1][0], { - text: 'test 2' + text: "test 2", }); }); - it('Can match text snapshot with dynamic URL query parameters', function () { + it("Can match text snapshot with dynamic URL query parameters", function () { emailMockReceiver.send({ - text: 'test https://127.0.0.1:2369/welcome/?token=JRexE3uutntD6F6WXSVaDZke91fTjpvO&action=signup' + text: "test https://127.0.0.1:2369/welcome/?token=JRexE3uutntD6F6WXSVaDZke91fTjpvO&action=signup", }); - emailMockReceiver.matchPlaintextSnapshot([{ - pattern: /token=(\w+)/gmi, - replacement: 'token=TEST_TOKEN' - }]); + emailMockReceiver.matchPlaintextSnapshot([ + { + pattern: /token=(\w+)/gim, + replacement: "token=TEST_TOKEN", + }, + ]); assert.equal(snapshotManager.assertSnapshot.calledOnce, true); assert.deepEqual(snapshotManager.assertSnapshot.args[0][0], { - text: 'test https://127.0.0.1:2369/welcome/?token=TEST_TOKEN&action=signup' + text: "test https://127.0.0.1:2369/welcome/?token=TEST_TOKEN&action=signup", }); }); - it('Can match text snapshot with dynamic version in content', function () { + it("Can match text snapshot with dynamic version in content", function () { emailMockReceiver.send({ - text: 'this email contains a dynamic version string v5.45' + text: "this email contains a dynamic version string v5.45", }); - emailMockReceiver.matchPlaintextSnapshot([{ - pattern: /v\d+.\d+/gmi, - replacement: 'v5.0' - }]); + emailMockReceiver.matchPlaintextSnapshot([ + { + pattern: /v\d+.\d+/gim, + replacement: "v5.0", + }, + ]); assert.equal(snapshotManager.assertSnapshot.calledOnce, true); assert.deepEqual(snapshotManager.assertSnapshot.args[0][0], { - text: 'this email contains a dynamic version string v5.0' + text: "this email contains a dynamic version string v5.0", }); }); - it('Cant match text snapshot with multiple occurrences of dynamic content', function () { + it("Cant match text snapshot with multiple occurrences of dynamic content", function () { emailMockReceiver.send({ - text: 'this email contains a dynamic version string once v5.45 and twice v4.28' + text: "this email contains a dynamic version string once v5.45 and twice v4.28", }); - emailMockReceiver.matchPlaintextSnapshot([{ - pattern: /v\d+.\d+/gmi, - replacement: 'v5.0' - }]); + emailMockReceiver.matchPlaintextSnapshot([ + { + pattern: /v\d+.\d+/gim, + replacement: "v5.0", + }, + ]); assert.equal(snapshotManager.assertSnapshot.calledOnce, true); assert.deepEqual(snapshotManager.assertSnapshot.args[0][0], { - text: 'this email contains a dynamic version string once v5.0 and twice v5.0' + text: "this email contains a dynamic version string once v5.0 and twice v5.0", }); }); }); - describe('matchMetadataSnapshot', function (){ - it('Can match primitive metadata snapshot ignoring html property', function () { + describe("matchMetadataSnapshot", function () { + it("Can match primitive metadata snapshot ignoring html property", function () { emailMockReceiver.send({ - subject: 'test', - to: 'test@example.com', - html: '
do not include me
' + subject: "test", + to: "test@example.com", + html: "
do not include me
", }); emailMockReceiver.matchMetadataSnapshot(); assert.equal(snapshotManager.assertSnapshot.calledOnce, true); assert.deepEqual(snapshotManager.assertSnapshot.args[0][0].metadata, { - subject: 'test', - to: 'test@example.com' + subject: "test", + to: "test@example.com", }); }); - it('Can match explicit second metadata snapshot when multiple send requests are executed', function () { + it("Can match explicit second metadata snapshot when multiple send requests are executed", function () { emailMockReceiver.send({ - subject: 'test 1', - to: 'test@example.com' + subject: "test 1", + to: "test@example.com", }); emailMockReceiver.send({ - subject: 'test 2', - to: 'test@example.com' + subject: "test 2", + to: "test@example.com", }); emailMockReceiver.matchMetadataSnapshot({}, 1); assert.equal(snapshotManager.assertSnapshot.calledOnce, true); assert.deepEqual(snapshotManager.assertSnapshot.args[0][0].metadata, { - subject: 'test 2', - to: 'test@example.com' + subject: "test 2", + to: "test@example.com", }); }); }); - describe('assertSentEmailCount', function () { - it('Can assert email count', function () { - emailMockReceiver.send({html: '
test
'}); + describe("assertSentEmailCount", function () { + it("Can assert email count", function () { + emailMockReceiver.send({ html: "
test
" }); emailMockReceiver.assertSentEmailCount(1); }); - it('Can assert email count with multiple send requests are executed', function () { - emailMockReceiver.send({html: '
test 1
'}); - emailMockReceiver.send({html: '
test 2
'}); + it("Can assert email count with multiple send requests are executed", function () { + emailMockReceiver.send({ html: "
test 1
" }); + emailMockReceiver.send({ html: "
test 2
" }); emailMockReceiver.assertSentEmailCount(2); }); - it('Can reset email count', function () { - emailMockReceiver.send({html: '
test 1
'}); + it("Can reset email count", function () { + emailMockReceiver.send({ html: "
test 1
" }); emailMockReceiver.assertSentEmailCount(1); emailMockReceiver.reset(); emailMockReceiver.assertSentEmailCount(0); }); - it('Throws error when email count is not equal to expected', function () { - emailMockReceiver.send({html: '
test 1
'}); + it("Throws error when email count is not equal to expected", function () { + emailMockReceiver.send({ html: "
test 1
" }); assert.throws(function () { emailMockReceiver.assertSentEmailCount(2); @@ -274,22 +286,22 @@ describe('Email mock receiver', function () { }); }); - describe('getSentEmail', function () { - it('Can get sent email', function () { - emailMockReceiver.send({html: '
test
'}); - assert.equal(emailMockReceiver.getSentEmail(0).html, '
test
'); + describe("getSentEmail", function () { + it("Can get sent email", function () { + emailMockReceiver.send({ html: "
test
" }); + assert.equal(emailMockReceiver.getSentEmail(0).html, "
test
"); }); - it('Can get sent email with multiple send requests are executed', function () { - emailMockReceiver.send({html: '
test 1
'}); - emailMockReceiver.send({html: '
test 2
'}); + it("Can get sent email with multiple send requests are executed", function () { + emailMockReceiver.send({ html: "
test 1
" }); + emailMockReceiver.send({ html: "
test 2
" }); - assert.equal(emailMockReceiver.getSentEmail(0).html, '
test 1
'); - assert.equal(emailMockReceiver.getSentEmail(1).html, '
test 2
'); + assert.equal(emailMockReceiver.getSentEmail(0).html, "
test 1
"); + assert.equal(emailMockReceiver.getSentEmail(1).html, "
test 2
"); }); - it('Returns undefined when email index is out of bounds', function () { - emailMockReceiver.send({html: '
test 1
'}); + it("Returns undefined when email index is out of bounds", function () { + emailMockReceiver.send({ html: "
test 1
" }); assert.equal(emailMockReceiver.getSentEmail(1), undefined); }); diff --git a/packages/errors/.eslintrc.js b/packages/errors/.eslintrc.js index cb690be63..1c61e22e1 100644 --- a/packages/errors/.eslintrc.js +++ b/packages/errors/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/ts' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/ts"], }; diff --git a/packages/errors/README.md b/packages/errors/README.md index e5845a41e..b9c458185 100644 --- a/packages/errors/README.md +++ b/packages/errors/README.md @@ -8,36 +8,30 @@ or `yarn add @tryghost/errors` - ## Purpose Shared Ghost error classes and utilities for typed errors, context propagation, and safe stack formatting. ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests - - - # Copyright & License Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/errors/package.json b/packages/errors/package.json index 99029e645..d5cf25575 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -1,18 +1,27 @@ { "name": "@tryghost/errors", "version": "3.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/errors" }, - "author": "Ghost Foundation", - "license": "MIT", + "source": "src/index.ts", + "files": [ + "cjs", + "es", + "types", + "src" + ], + "sideEffects": false, "main": "cjs/index.js", "module": "es/index.js", "types": "types/index.d.ts", - "source": "src/index.ts", - "sideEffects": false, + "publishConfig": { + "access": "public" + }, "scripts": { "dev": "echo \"Implement me!\"", "prepare": "NODE_ENV=production yarn build", @@ -26,21 +35,12 @@ "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, - "files": [ - "cjs", - "es", - "types", - "src" - ], - "publishConfig": { - "access": "public" - }, + "dependencies": {}, "devDependencies": { "@types/lodash": "4.17.24", "esbuild": "0.27.3", "lodash": "4.17.23", "ts-node": "10.9.2", "typescript": "5.9.3" - }, - "dependencies": {} + } } diff --git a/packages/errors/src/GhostError.ts b/packages/errors/src/GhostError.ts index d5d638a0f..9e009f1c4 100644 --- a/packages/errors/src/GhostError.ts +++ b/packages/errors/src/GhostError.ts @@ -1,5 +1,5 @@ -import {randomUUID} from 'crypto'; -import {wrapStack} from './wrap-stack'; +import { randomUUID } from "crypto"; +import { wrapStack } from "./wrap-stack"; export interface GhostErrorOptions { message?: string; @@ -39,9 +39,9 @@ export class GhostError extends Error { * defaults */ this.statusCode = 500; - this.errorType = 'InternalServerError'; - this.level = 'normal'; - this.message = 'The server has encountered an error.'; + this.errorType = "InternalServerError"; + this.level = "normal"; + this.message = "The server has encountered an error."; this.id = randomUUID(); /** @@ -65,25 +65,27 @@ export class GhostError extends Error { // Nested objects are getting copied over in one piece (can be changed, but not needed right now) if (options.err) { // CASE: Support err as string (it happens that third party libs return a string instead of an error instance) - if (typeof options.err === 'string') { + if (typeof options.err === "string") { /* eslint-disable no-restricted-syntax */ options.err = new Error(options.err); /* eslint-enable no-restricted-syntax */ } Object.getOwnPropertyNames(options.err).forEach((property) => { - if (['errorType', 'name', 'statusCode', 'message', 'level'].indexOf(property) !== -1) { + if ( + ["errorType", "name", "statusCode", "message", "level"].indexOf(property) !== -1 + ) { return; } // CASE: `code` should put options as priority over err - if (property === 'code') { + if (property === "code") { // eslint-disable-next-line @typescript-eslint/no-explicit-any this[property] = this[property] || (options.err as any)[property]; return; } - if (property === 'stack' && !this.hideStack) { + if (property === "stack" && !this.hideStack) { this[property] = wrapStack(this, options.err as Error); return; } diff --git a/packages/errors/src/errors.ts b/packages/errors/src/errors.ts index 2bd841466..c9cd25063 100644 --- a/packages/errors/src/errors.ts +++ b/packages/errors/src/errors.ts @@ -1,7 +1,7 @@ -import {GhostError, GhostErrorOptions} from './GhostError'; +import { GhostError, GhostErrorOptions } from "./GhostError"; const mergeOptions = (options: GhostErrorOptions, defaults: GhostErrorOptions) => { - const result = {...defaults}; + const result = { ...defaults }; // Ignore undefined options - for example passing statusCode: undefined should not override the default (Object.keys(options) as (keyof GhostErrorOptions)[]).forEach((key) => { @@ -15,298 +15,357 @@ const mergeOptions = (options: GhostErrorOptions, defaults: GhostErrorOptions) = export class InternalServerError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - statusCode: 500, - level: 'critical', - errorType: 'InternalServerError', - message: 'The server has encountered an error.' - })); + super( + mergeOptions(options, { + statusCode: 500, + level: "critical", + errorType: "InternalServerError", + message: "The server has encountered an error.", + }), + ); } } export class IncorrectUsageError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - statusCode: 400, - level: 'critical', - errorType: 'IncorrectUsageError', - message: 'We detected a misuse. Please read the stack trace.' - })); + super( + mergeOptions(options, { + statusCode: 400, + level: "critical", + errorType: "IncorrectUsageError", + message: "We detected a misuse. Please read the stack trace.", + }), + ); } } export class NotFoundError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - statusCode: 404, - errorType: 'NotFoundError', - message: 'Resource could not be found.', - hideStack: true - })); + super( + mergeOptions(options, { + statusCode: 404, + errorType: "NotFoundError", + message: "Resource could not be found.", + hideStack: true, + }), + ); } } export class BadRequestError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - statusCode: 400, - errorType: 'BadRequestError', - message: 'The request could not be understood.' - })); + super( + mergeOptions(options, { + statusCode: 400, + errorType: "BadRequestError", + message: "The request could not be understood.", + }), + ); } } export class UnauthorizedError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - statusCode: 401, - errorType: 'UnauthorizedError', - message: 'You are not authorised to make this request.' - })); + super( + mergeOptions(options, { + statusCode: 401, + errorType: "UnauthorizedError", + message: "You are not authorised to make this request.", + }), + ); } } export class NoPermissionError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - statusCode: 403, - errorType: 'NoPermissionError', - message: 'You do not have permission to perform this request.' - })); + super( + mergeOptions(options, { + statusCode: 403, + errorType: "NoPermissionError", + message: "You do not have permission to perform this request.", + }), + ); } } export class ValidationError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - statusCode: 422, - errorType: 'ValidationError', - message: 'The request failed validation.' - })); + super( + mergeOptions(options, { + statusCode: 422, + errorType: "ValidationError", + message: "The request failed validation.", + }), + ); } } export class UnsupportedMediaTypeError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - statusCode: 415, - errorType: 'UnsupportedMediaTypeError', - message: 'The media in the request is not supported by the server.' - })); + super( + mergeOptions(options, { + statusCode: 415, + errorType: "UnsupportedMediaTypeError", + message: "The media in the request is not supported by the server.", + }), + ); } } export class TooManyRequestsError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - statusCode: 429, - errorType: 'TooManyRequestsError', - message: 'Server has received too many similar requests in a short space of time.' - })); + super( + mergeOptions(options, { + statusCode: 429, + errorType: "TooManyRequestsError", + message: "Server has received too many similar requests in a short space of time.", + }), + ); } } export class MaintenanceError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - statusCode: 503, - errorType: 'MaintenanceError', - message: 'The server is temporarily down for maintenance.' - })); + super( + mergeOptions(options, { + statusCode: 503, + errorType: "MaintenanceError", + message: "The server is temporarily down for maintenance.", + }), + ); } } export class MethodNotAllowedError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - statusCode: 405, - errorType: 'MethodNotAllowedError', - message: 'Method not allowed for resource.' - })); + super( + mergeOptions(options, { + statusCode: 405, + errorType: "MethodNotAllowedError", + message: "Method not allowed for resource.", + }), + ); } } export class RequestNotAcceptableError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - statusCode: 406, - errorType: 'RequestNotAcceptableError', - message: 'Request not acceptable for provided Accept-Version header.', - hideStack: true - })); + super( + mergeOptions(options, { + statusCode: 406, + errorType: "RequestNotAcceptableError", + message: "Request not acceptable for provided Accept-Version header.", + hideStack: true, + }), + ); } } export class RangeNotSatisfiableError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - statusCode: 416, - errorType: 'RangeNotSatisfiableError', - message: 'Range not satisfiable for provided Range header.', - hideStack: true - })); + super( + mergeOptions(options, { + statusCode: 416, + errorType: "RangeNotSatisfiableError", + message: "Range not satisfiable for provided Range header.", + hideStack: true, + }), + ); } } export class RequestEntityTooLargeError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - statusCode: 413, - errorType: 'RequestEntityTooLargeError', - message: 'Request was too big for the server to handle.' - })); + super( + mergeOptions(options, { + statusCode: 413, + errorType: "RequestEntityTooLargeError", + message: "Request was too big for the server to handle.", + }), + ); } } export class TokenRevocationError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - statusCode: 503, - errorType: 'TokenRevocationError', - message: 'Token is no longer available.' - })); + super( + mergeOptions(options, { + statusCode: 503, + errorType: "TokenRevocationError", + message: "Token is no longer available.", + }), + ); } } export class VersionMismatchError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - statusCode: 400, - errorType: 'VersionMismatchError', - message: 'Requested version does not match server version.' - })); + super( + mergeOptions(options, { + statusCode: 400, + errorType: "VersionMismatchError", + message: "Requested version does not match server version.", + }), + ); } } export class DataExportError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - statusCode: 500, - errorType: 'DataExportError', - message: 'The server encountered an error whilst exporting data.' - })); + super( + mergeOptions(options, { + statusCode: 500, + errorType: "DataExportError", + message: "The server encountered an error whilst exporting data.", + }), + ); } } export class DataImportError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - statusCode: 500, - errorType: 'DataImportError', - message: 'The server encountered an error whilst importing data.' - })); + super( + mergeOptions(options, { + statusCode: 500, + errorType: "DataImportError", + message: "The server encountered an error whilst importing data.", + }), + ); } } export class EmailError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - statusCode: 500, - errorType: 'EmailError', - message: 'The server encountered an error whilst sending email.' - })); + super( + mergeOptions(options, { + statusCode: 500, + errorType: "EmailError", + message: "The server encountered an error whilst sending email.", + }), + ); } } export class ThemeValidationError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - statusCode: 422, - errorType: 'ThemeValidationError', - message: 'The theme has a validation error.', - errorDetails: {} - })); + super( + mergeOptions(options, { + statusCode: 422, + errorType: "ThemeValidationError", + message: "The theme has a validation error.", + errorDetails: {}, + }), + ); } } export class DisabledFeatureError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - statusCode: 409, - errorType: 'DisabledFeatureError', - message: 'Unable to complete the request, this feature is disabled.' - })); + super( + mergeOptions(options, { + statusCode: 409, + errorType: "DisabledFeatureError", + message: "Unable to complete the request, this feature is disabled.", + }), + ); } } export class UpdateCollisionError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - statusCode: 409, - errorType: 'UpdateCollisionError', - message: 'Unable to complete the request, there was a conflict.' - })); + super( + mergeOptions(options, { + statusCode: 409, + errorType: "UpdateCollisionError", + message: "Unable to complete the request, there was a conflict.", + }), + ); } } export class HostLimitError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - errorType: 'HostLimitError', - hideStack: true, - statusCode: 403, - message: 'Unable to complete the request, this resource is limited.' - })); + super( + mergeOptions(options, { + errorType: "HostLimitError", + hideStack: true, + statusCode: 403, + message: "Unable to complete the request, this resource is limited.", + }), + ); } } export class HelperWarning extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - errorType: 'HelperWarning', - hideStack: true, - statusCode: 400, - message: 'A theme helper has done something unexpected.' - })); + super( + mergeOptions(options, { + errorType: "HelperWarning", + hideStack: true, + statusCode: 400, + message: "A theme helper has done something unexpected.", + }), + ); } } export class PasswordResetRequiredError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - errorType: 'PasswordResetRequiredError', - statusCode: 401, - message: 'For security, you need to create a new password. An email has been sent to you with instructions!' - })); + super( + mergeOptions(options, { + errorType: "PasswordResetRequiredError", + statusCode: 401, + message: + "For security, you need to create a new password. An email has been sent to you with instructions!", + }), + ); } } export class UnhandledJobError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - errorType: 'UnhandledJobError', - message: 'Processed job threw an unhandled error', - level: 'critical' - })); + super( + mergeOptions(options, { + errorType: "UnhandledJobError", + message: "Processed job threw an unhandled error", + level: "critical", + }), + ); } } export class NoContentError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - errorType: 'NoContentError', - statusCode: 204, - hideStack: true - })); + super( + mergeOptions(options, { + errorType: "NoContentError", + statusCode: 204, + hideStack: true, + }), + ); } } export class ConflictError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - errorType: 'ConflictError', - statusCode: 409, - message: 'The server has encountered an conflict.' - })); + super( + mergeOptions(options, { + errorType: "ConflictError", + statusCode: 409, + message: "The server has encountered an conflict.", + }), + ); } } export class MigrationError extends GhostError { constructor(options: GhostErrorOptions = {}) { - super(mergeOptions(options, { - errorType: 'MigrationError', - message: 'An error has occurred applying a database migration.', - level: 'critical' - })); + super( + mergeOptions(options, { + errorType: "MigrationError", + message: "An error has occurred applying a database migration.", + level: "critical", + }), + ); } } diff --git a/packages/errors/src/index.ts b/packages/errors/src/index.ts index 2d627440d..80e0a22ac 100644 --- a/packages/errors/src/index.ts +++ b/packages/errors/src/index.ts @@ -1,14 +1,14 @@ -import {GhostError} from './GhostError'; -import * as ghostErrors from './errors'; -import {deserialize, isGhostError, prepareStackForUser, serialize} from './utils'; +import { GhostError } from "./GhostError"; +import * as ghostErrors from "./errors"; +import { deserialize, isGhostError, prepareStackForUser, serialize } from "./utils"; -export * from './errors'; -export type {GhostError}; +export * from "./errors"; +export type { GhostError }; export default ghostErrors; export const utils = { serialize, deserialize, isGhostError, - prepareStackForUser + prepareStackForUser, }; diff --git a/packages/errors/src/utils.ts b/packages/errors/src/utils.ts index 2a34cb1cc..8ed4a07a8 100644 --- a/packages/errors/src/utils.ts +++ b/packages/errors/src/utils.ts @@ -1,14 +1,14 @@ -import {GhostError} from './GhostError'; -import * as errors from './errors'; +import { GhostError } from "./GhostError"; +import * as errors from "./errors"; // eslint-disable-next-line @typescript-eslint/no-explicit-any -type AnyObject = Record +type AnyObject = Record; // structuredClone doesn't preserve custom properties on Error subclasses // (see https://github.com/ungap/structured-clone/issues/12) // eslint-disable-next-line @typescript-eslint/no-explicit-any function deepCloneValue(value: any): any { - if (value === null || typeof value !== 'object') { + if (value === null || typeof value !== "object") { return value; } if (value instanceof Error) { @@ -28,7 +28,7 @@ function deepCloneValue(value: any): any { return clone; } -const errorsWithBase: Record = {...errors, GhostError}; +const errorsWithBase: Record = { ...errors, GhostError }; const _private = { serialize(err: GhostError) { @@ -44,12 +44,12 @@ const _private = { help: err.help, errorDetails: err.errorDetails, level: err.level, - errorType: err.errorType - } + errorType: err.errorType, + }, }; } catch (error) { return { - detail: 'Something went wrong.' + detail: "Something went wrong.", }; } }, @@ -62,7 +62,7 @@ const _private = { code: obj.code || obj.error, level: obj.meta && obj.meta.level, help: obj.meta && obj.meta.help, - context: obj.meta && obj.meta.context + context: obj.meta && obj.meta.context, }; }, @@ -75,20 +75,20 @@ const _private = { */ OAuthSerialize(err: GhostError) { const matchTable = { - [errors.NoPermissionError.name]: 'access_denied', - [errors.MaintenanceError.name]: 'temporarily_unavailable', - [errors.BadRequestError.name]: 'invalid_request', - [errors.ValidationError.name]: 'invalid_request', - default: 'server_error' + [errors.NoPermissionError.name]: "access_denied", + [errors.MaintenanceError.name]: "temporarily_unavailable", + [errors.BadRequestError.name]: "invalid_request", + [errors.ValidationError.name]: "invalid_request", + default: "server_error", }; // eslint-disable-next-line @typescript-eslint/no-unused-vars - const {detail, code, ...properties} = _private.serialize(err); + const { detail, code, ...properties } = _private.serialize(err); return { - error: err.code || matchTable[err.name] || 'server_error', + error: err.code || matchTable[err.name] || "server_error", error_description: err.message, - ...properties + ...properties, }; }, @@ -98,12 +98,14 @@ const _private = { */ OAuthDeserialize(errorFormat: AnyObject): GhostError { try { - return new errorsWithBase[errorFormat.title || errorFormat.name || errors.InternalServerError.name](_private.deserialize(errorFormat)); + return new errorsWithBase[ + errorFormat.title || errorFormat.name || errors.InternalServerError.name + ](_private.deserialize(errorFormat)); } catch (err) { // CASE: you receive an OAuth formatted error, but the error prototype is unknown return new errors.InternalServerError({ errorType: errorFormat.title || errorFormat.name, - ..._private.deserialize(errorFormat) + ..._private.deserialize(errorFormat), }); } }, @@ -115,13 +117,13 @@ const _private = { */ JSONAPISerialize(err: GhostError): AnyObject { const errorFormat: AnyObject = { - errors: [_private.serialize(err)] + errors: [_private.serialize(err)], }; errorFormat.errors[0].source = {}; if (err.property) { - errorFormat.errors[0].source.pointer = '/data/attributes/' + err.property; + errorFormat.errors[0].source.pointer = "/data/attributes/" + err.property; } return errorFormat; @@ -131,26 +133,28 @@ const _private = { * @description Deserialize JSON api format into GhostError instance. */ JSONAPIDeserialize(errorFormat: AnyObject): GhostError { - errorFormat = errorFormat.errors && errorFormat.errors[0] || {}; + errorFormat = (errorFormat.errors && errorFormat.errors[0]) || {}; let internalError; try { - internalError = new errorsWithBase[errorFormat.title || errorFormat.name || errors.InternalServerError.name](_private.deserialize(errorFormat)); + internalError = new errorsWithBase[ + errorFormat.title || errorFormat.name || errors.InternalServerError.name + ](_private.deserialize(errorFormat)); } catch (err) { // CASE: you receive a JSON format error, but the error prototype is unknown internalError = new errors.InternalServerError({ errorType: errorFormat.title || errorFormat.name, - ..._private.deserialize(errorFormat) + ..._private.deserialize(errorFormat), }); } if (errorFormat.source && errorFormat.source.pointer) { - internalError.property = errorFormat.source.pointer.split('/')[3]; + internalError.property = errorFormat.source.pointer.split("/")[3]; } return internalError; - } + }, }; /** @@ -165,24 +169,24 @@ const _private = { * * @see http://jsonapi.org/format/#errors */ -export function serialize(err: GhostError, options?: {format: 'jsonapi' | 'oauth'}) { - options = options || {format: 'jsonapi'}; +export function serialize(err: GhostError, options?: { format: "jsonapi" | "oauth" }) { + options = options || { format: "jsonapi" }; let errorFormat: AnyObject = {}; try { - if (options.format === 'jsonapi') { + if (options.format === "jsonapi") { errorFormat = _private.JSONAPISerialize(err); } else { errorFormat = _private.OAuthSerialize(err); } } catch (error) { - errorFormat.message = 'Something went wrong.'; + errorFormat.message = "Something went wrong."; } // no need to sanitize the undefined values, on response send JSON.stringify get's called return errorFormat; -}; +} /** * @description Deserialize from error JSON format to GhostError instance @@ -197,22 +201,22 @@ export function deserialize(errorFormat: AnyObject): AnyObject { } return internalError; -}; +} /** * @description Replace the stack with a user-facing one * @returns Clone of the original error with a user-facing stack */ -export function prepareStackForUser(error: GhostError): GhostError -export function prepareStackForUser(error: Error): Error +export function prepareStackForUser(error: GhostError): GhostError; +export function prepareStackForUser(error: Error): Error; export function prepareStackForUser(error: Error): Error { const stackbits = error.stack?.split(/\n/) || []; // We build this up backwards, so we always insert at position 1 - const hideStack = 'hideStack' in error && error.hideStack; + const hideStack = "hideStack" in error && error.hideStack; - if (process.env.NODE_ENV === 'production' || hideStack) { + if (process.env.NODE_ENV === "production" || hideStack) { stackbits.splice(1, stackbits.length - 1); } else { // Clearly mark the strack trace @@ -220,25 +224,25 @@ export function prepareStackForUser(error: Error): Error { } // Add in our custom context and help methods - if ('help' in error && error.help) { + if ("help" in error && error.help) { stackbits.splice(1, 0, `${error.help}`); } - if ('context' in error && error.context) { + if ("context" in error && error.context) { stackbits.splice(1, 0, `${error.context}`); } const errorClone = deepCloneValue(error); - errorClone.stack = stackbits.join('\n'); + errorClone.stack = stackbits.join("\n"); return errorClone; -}; +} /** * @description Check whether an error instance is a GhostError. */ export function isGhostError(err: Error) { const errorName = GhostError.name; - const legacyErrorName = 'IgnitionError'; + const legacyErrorName = "IgnitionError"; const recursiveIsGhostError = function recursiveIsGhostError(obj: AnyObject): boolean { // no super constructor available anymore @@ -254,4 +258,4 @@ export function isGhostError(err: Error) { }; return recursiveIsGhostError(err.constructor); -}; +} diff --git a/packages/errors/src/wrap-stack.ts b/packages/errors/src/wrap-stack.ts index 2b6f17531..07a0c9e40 100644 --- a/packages/errors/src/wrap-stack.ts +++ b/packages/errors/src/wrap-stack.ts @@ -1,5 +1,5 @@ export function wrapStack(err: Error, internalErr: Error) { const extraLine = (err.stack?.split(/\n/g) || [])[1]; const [firstLine, ...rest] = internalErr.stack?.split(/\n/g) || []; - return [firstLine, extraLine, ...rest].join('\n'); -}; + return [firstLine, extraLine, ...rest].join("\n"); +} diff --git a/packages/errors/test/.eslintrc.js b/packages/errors/test/.eslintrc.js index 42f8e7735..a10d22c75 100644 --- a/packages/errors/test/.eslintrc.js +++ b/packages/errors/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/ts-test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/ts-test"], }; diff --git a/packages/errors/test/errors.test.ts b/packages/errors/test/errors.test.ts index 355c6870e..26a949508 100644 --- a/packages/errors/test/errors.test.ts +++ b/packages/errors/test/errors.test.ts @@ -1,11 +1,11 @@ -import assert from 'assert/strict'; -import errors, {utils} from '../src'; -import {GhostError} from '../src/GhostError'; +import assert from "assert/strict"; +import errors, { utils } from "../src"; +import { GhostError } from "../src/GhostError"; type GhostErrorConstructor = new (options?: Record) => GhostError; function containsSubset(actual: any, expected: any): boolean { - if (expected === null || typeof expected !== 'object') { + if (expected === null || typeof expected !== "object") { return actual === expected; } @@ -20,8 +20,8 @@ function expectJSONErrorResponse(serialized: any, expected: any) { assert.ok(serialized.errors.length > 0); for (const err of serialized.errors) { - assert.equal(typeof err, 'object'); - for (const key of ['id', 'title', 'detail', 'status', 'code', 'meta']) { + assert.equal(typeof err, "object"); + for (const key of ["id", "title", "detail", "status", "code", "meta"]) { assert.ok(Object.prototype.hasOwnProperty.call(err, key)); } } @@ -29,152 +29,162 @@ function expectJSONErrorResponse(serialized: any, expected: any) { assert.ok(serialized.errors.some((err: any) => containsSubset(err, expected))); } -describe('Errors', function () { - it('Ensure we inherit from Error', function () { +describe("Errors", function () { + it("Ensure we inherit from Error", function () { const ghostError = new errors.InternalServerError(); assert.equal(ghostError instanceof Error, true); }); - describe('Inherit from other error', function () { - it('default', function () { - const someError = new Error() as Error & {context?: string; help?: string}; - someError.message = 'test'; - someError.context = 'test'; - someError.help = 'test'; + describe("Inherit from other error", function () { + it("default", function () { + const someError = new Error() as Error & { context?: string; help?: string }; + someError.message = "test"; + someError.context = "test"; + someError.help = "test"; - const ghostError = new errors.InternalServerError({err: someError}); + const ghostError = new errors.InternalServerError({ err: someError }); assert.match(ghostError.stack!, /Error: test/); assert.equal(ghostError.context, someError.context); assert.equal(ghostError.help, someError.help); }); - it('has nested object', function () { - const someError = new Error() as Error & {obj?: {a: string}}; - someError.obj = {a: 'b'}; + it("has nested object", function () { + const someError = new Error() as Error & { obj?: { a: string } }; + someError.obj = { a: "b" }; - const ghostError = new errors.InternalServerError({err: someError}) as Error & {obj?: {a: string}}; + const ghostError = new errors.InternalServerError({ err: someError }) as Error & { + obj?: { a: string }; + }; assert.deepEqual(ghostError.obj, someError.obj); }); - it('with custom attribute', function () { - const someError = new Error() as Error & {context?: string}; - someError.context = 'test'; + it("with custom attribute", function () { + const someError = new Error() as Error & { context?: string }; + someError.context = "test"; - const ghostError = new errors.InternalServerError({err: someError, context: 'context'}); - assert.equal(ghostError.context, 'test'); + const ghostError = new errors.InternalServerError({ + err: someError, + context: "context", + }); + assert.equal(ghostError.context, "test"); }); - it('does not overwrite key attributes', function () { - const someError = new Error() as Error & {errorType?: string; statusCode?: number; level?: string; code?: string}; - someError.errorType = 'test'; - someError.name = 'test'; + it("does not overwrite key attributes", function () { + const someError = new Error() as Error & { + errorType?: string; + statusCode?: number; + level?: string; + code?: string; + }; + someError.errorType = "test"; + someError.name = "test"; someError.statusCode = 0; - someError.message = 'test'; - someError.level = 'test'; - someError.code = 'TEST_CODE'; + someError.message = "test"; + someError.level = "test"; + someError.code = "TEST_CODE"; - const ghostError = new errors.InternalServerError({err: someError, code: 'CODE'}); + const ghostError = new errors.InternalServerError({ err: someError, code: "CODE" }); - assert.equal(ghostError.errorType, 'InternalServerError'); - assert.equal(ghostError.name, 'InternalServerError'); + assert.equal(ghostError.errorType, "InternalServerError"); + assert.equal(ghostError.name, "InternalServerError"); assert.equal(ghostError.statusCode, 500); - assert.equal(ghostError.message, 'The server has encountered an error.'); - assert.equal(ghostError.level, 'critical'); - assert.equal(ghostError.code, 'CODE'); + assert.equal(ghostError.message, "The server has encountered an error."); + assert.equal(ghostError.level, "critical"); + assert.equal(ghostError.code, "CODE"); }); - it('defaults to the original error code', function () { - const someError = new Error() as Error & {code?: string}; - someError.code = 'TEST_CODE'; + it("defaults to the original error code", function () { + const someError = new Error() as Error & { code?: string }; + someError.code = "TEST_CODE"; - const ghostError = new errors.InternalServerError({err: someError}); - assert.equal(ghostError.code, 'TEST_CODE'); + const ghostError = new errors.InternalServerError({ err: someError }); + assert.equal(ghostError.code, "TEST_CODE"); }); - it('with custom message', function () { + it("with custom message", function () { const someError = new Error(); - const ghostError = new errors.InternalServerError({err: someError, message: 'test'}); - assert.equal(ghostError.message, 'test'); + const ghostError = new errors.InternalServerError({ err: someError, message: "test" }); + assert.equal(ghostError.message, "test"); }); - it('error is string', function () { - const ghostError = new errors.InternalServerError({err: 'string'}); + it("error is string", function () { + const ghostError = new errors.InternalServerError({ err: "string" }); assert.match(ghostError.stack!, /Error: string/); }); - it('supports explicit errorType option and mirrors name', function () { - const error = new GhostError({errorType: 'CustomErrorType'}); - assert.equal(error.errorType, 'CustomErrorType'); - assert.equal(error.name, 'CustomErrorType'); + it("supports explicit errorType option and mirrors name", function () { + const error = new GhostError({ errorType: "CustomErrorType" }); + assert.equal(error.errorType, "CustomErrorType"); + assert.equal(error.name, "CustomErrorType"); }); - it('uses default errorType when one is not provided', function () { + it("uses default errorType when one is not provided", function () { const error = new GhostError(); - assert.equal(error.errorType, 'InternalServerError'); - assert.equal(error.name, 'InternalServerError'); + assert.equal(error.errorType, "InternalServerError"); + assert.equal(error.name, "InternalServerError"); }); - it('preserves existing property values when inherited error property is falsy', function () { - const someError = new Error() as Error & {context?: string}; - someError.context = ''; + it("preserves existing property values when inherited error property is falsy", function () { + const someError = new Error() as Error & { context?: string }; + someError.context = ""; const ghostError = new errors.InternalServerError({ err: someError, - context: 'context-value' + context: "context-value", }); - assert.equal(ghostError.context, 'context-value'); + assert.equal(ghostError.context, "context-value"); }); }); - describe('isGhostError', function () { - it('can determine non-Ghost errors', function () { + describe("isGhostError", function () { + it("can determine non-Ghost errors", function () { assert.equal(utils.isGhostError(new Error()), false); }); - it('can determine standard GhostError errors', function () { + it("can determine standard GhostError errors", function () { assert.equal(utils.isGhostError(new errors.NotFoundError()), true); }); - it('can determine new non-GhostError errors', function () { + it("can determine new non-GhostError errors", function () { class NonGhostError extends Error { - constructor(options: {message: string}) { + constructor(options: { message: string }) { super(options.message); } } class CustomNonGhostError extends NonGhostError { - constructor(options: {message: string}) { + constructor(options: { message: string }) { super(options); } } - const err = new CustomNonGhostError({message: 'Does not inherit from GhostError'}); + const err = new CustomNonGhostError({ message: "Does not inherit from GhostError" }); assert.equal(utils.isGhostError(err), false); }); }); - describe('Serialization', function () { - it('Can serialize/deserialize error', function () { + describe("Serialization", function () { + it("Can serialize/deserialize error", function () { let err = new errors.BadRequestError({ - help: 'do you need help?', - context: 'i cannot help', - property: 'email' + help: "do you need help?", + context: "i cannot help", + property: "email", }); let serialized = utils.serialize(err); expectJSONErrorResponse(serialized, { status: 400, - code: 'BadRequestError', - title: 'BadRequestError', - detail: 'The request could not be understood.', - source: {pointer: '/data/attributes/email'}, + code: "BadRequestError", + title: "BadRequestError", + detail: "The request could not be understood.", + source: { pointer: "/data/attributes/email" }, meta: { - level: 'normal', - errorType: 'BadRequestError', - context: 'i cannot help', - help: 'do you need help?' - } + level: "normal", + errorType: "BadRequestError", + context: "i cannot help", + help: "do you need help?", + }, }); const deserialized = utils.deserialize(serialized); @@ -186,45 +196,54 @@ describe('Errors', function () { assert.equal(deserialized.level, serialized.errors[0].meta.level); assert.equal(deserialized.help, serialized.errors[0].meta.help); assert.equal(deserialized.context, serialized.errors[0].meta.context); - assert.equal(deserialized.property, 'email'); + assert.equal(deserialized.property, "email"); err = new errors.BadRequestError(); serialized = utils.serialize(err); expectJSONErrorResponse(serialized, { status: 400, - code: 'BadRequestError', - title: 'BadRequestError', - detail: 'The request could not be understood.', + code: "BadRequestError", + title: "BadRequestError", + detail: "The request could not be understood.", meta: { - level: 'normal', - errorType: 'BadRequestError' - } + level: "normal", + errorType: "BadRequestError", + }, }); assert.equal(serialized.errors[0].error, undefined); assert.equal(serialized.errors[0].error_description, undefined); }); - it('cannot serialize nothing', function () { - assert.equal((utils.serialize(undefined as any) as any).message, 'Something went wrong.'); + it("cannot serialize nothing", function () { + assert.equal( + (utils.serialize(undefined as any) as any).message, + "Something went wrong.", + ); }); - it('deserializing nothing results in a plain InternalServerError (the default)', function () { - assert.equal(utils.deserialize({}).message, 'The server has encountered an error.'); - assert.equal(utils.deserialize({errors: null as any}).message, 'The server has encountered an error.'); - assert.equal(utils.deserialize({errors: [] as any[]}).message, 'The server has encountered an error.'); + it("deserializing nothing results in a plain InternalServerError (the default)", function () { + assert.equal(utils.deserialize({}).message, "The server has encountered an error."); + assert.equal( + utils.deserialize({ errors: null as any }).message, + "The server has encountered an error.", + ); + assert.equal( + utils.deserialize({ errors: [] as any[] }).message, + "The server has encountered an error.", + ); }); - it('oauth serialize', function () { - const err = new errors.NoPermissionError({message: 'Permissions you need to have.'}); - const serialized = utils.serialize(err, {format: 'oauth'} as any); + it("oauth serialize", function () { + const err = new errors.NoPermissionError({ message: "Permissions you need to have." }); + const serialized = utils.serialize(err, { format: "oauth" } as any); - assert.equal(serialized.error, 'access_denied'); - assert.equal(serialized.error_description, 'Permissions you need to have.'); + assert.equal(serialized.error, "access_denied"); + assert.equal(serialized.error_description, "Permissions you need to have."); assert.equal(serialized.status, 403); - assert.equal(serialized.title, 'NoPermissionError'); - assert.equal(serialized.meta.level, 'normal'); + assert.equal(serialized.title, "NoPermissionError"); + assert.equal(serialized.meta.level, "normal"); assert.equal(serialized.message, undefined); assert.equal(serialized.detail, undefined); @@ -241,136 +260,417 @@ describe('Errors', function () { assert.equal(deserialized.level, serialized.meta.level); }); - it('[success] deserialize jsonapi, but target error name is unknown', function () { + it("[success] deserialize jsonapi, but target error name is unknown", function () { const deserialized = utils.deserialize({ - errors: [{ - name: 'UnknownError', - message: 'message' - }] + errors: [ + { + name: "UnknownError", + message: "message", + }, + ], }); assert.equal(deserialized instanceof errors.InternalServerError, true); assert.equal(deserialized instanceof Error, true); - assert.equal(deserialized.errorType, 'UnknownError'); - assert.equal(deserialized.message, 'message'); + assert.equal(deserialized.errorType, "UnknownError"); + assert.equal(deserialized.message, "message"); }); - it('[failure] deserialize oauth, but name is not an error name', function () { - const deserialized = utils.deserialize({name: 'random_oauth_error'} as any); + it("[failure] deserialize oauth, but name is not an error name", function () { + const deserialized = utils.deserialize({ name: "random_oauth_error" } as any); assert.equal(deserialized instanceof errors.InternalServerError, true); assert.equal(deserialized instanceof Error, true); }); - it('[failure] serialize oauth, but obj is empty', function () { - const serialized = utils.serialize({} as GhostError, {format: 'oauth'} as any); - assert.equal(serialized.error, 'server_error'); + it("[failure] serialize oauth, but obj is empty", function () { + const serialized = utils.serialize({} as GhostError, { format: "oauth" } as any); + assert.equal(serialized.error, "server_error"); }); - it('deserialize jsonapi known title path', function () { + it("deserialize jsonapi known title path", function () { const deserialized = utils.deserialize({ - errors: [{ - title: 'BadRequestError', - detail: 'bad', - status: 400, - code: 'BadRequestError', - meta: {level: 'normal'}, - source: { - pointer: '/data/attributes/email' - } - }] + errors: [ + { + title: "BadRequestError", + detail: "bad", + status: 400, + code: "BadRequestError", + meta: { level: "normal" }, + source: { + pointer: "/data/attributes/email", + }, + }, + ], }); assert.equal(deserialized instanceof errors.BadRequestError, true); - assert.equal(deserialized.message, 'bad'); - assert.equal(deserialized.property, 'email'); + assert.equal(deserialized.message, "bad"); + assert.equal(deserialized.property, "email"); }); }); - describe('prepareStackForUser', function () { - it('Correctly adds Stack Trace header line', function () { + describe("prepareStackForUser", function () { + it("Correctly adds Stack Trace header line", function () { const testStack = `Error: Line 0 - Message\nStack Line 1\nStack Line 2`; - const error = new Error('Test'); + const error = new Error("Test"); error.stack = testStack; - const {stack} = utils.prepareStackForUser(error); - assert.equal(stack, `Error: Line 0 - Message\nStack Trace:\nStack Line 1\nStack Line 2`); + const { stack } = utils.prepareStackForUser(error); + assert.equal( + stack, + `Error: Line 0 - Message\nStack Trace:\nStack Line 1\nStack Line 2`, + ); }); - it('Injects context', function () { - const error = new Error('Test') as Error & {context?: string}; + it("Injects context", function () { + const error = new Error("Test") as Error & { context?: string }; error.stack = `Error: Line 0 - Message\nStack Line 1\nStack Line 2`; - error.context = 'Line 1 - Context'; + error.context = "Line 1 - Context"; - const {stack} = utils.prepareStackForUser(error); - assert.equal(stack, `Error: Line 0 - Message\nLine 1 - Context\nStack Trace:\nStack Line 1\nStack Line 2`); + const { stack } = utils.prepareStackForUser(error); + assert.equal( + stack, + `Error: Line 0 - Message\nLine 1 - Context\nStack Trace:\nStack Line 1\nStack Line 2`, + ); }); - it('Injects help', function () { - const error = new Error('Test') as Error & {help?: string}; + it("Injects help", function () { + const error = new Error("Test") as Error & { help?: string }; error.stack = `Error: Line 0 - Message\nStack Line 1\nStack Line 2`; - error.help = 'Line 2 - Help'; + error.help = "Line 2 - Help"; - const {stack} = utils.prepareStackForUser(error); - assert.equal(stack, `Error: Line 0 - Message\nLine 2 - Help\nStack Trace:\nStack Line 1\nStack Line 2`); + const { stack } = utils.prepareStackForUser(error); + assert.equal( + stack, + `Error: Line 0 - Message\nLine 2 - Help\nStack Trace:\nStack Line 1\nStack Line 2`, + ); }); - it('Injects help & context', function () { - const error = new Error('Test') as Error & {context?: string; help?: string}; + it("Injects help & context", function () { + const error = new Error("Test") as Error & { context?: string; help?: string }; error.stack = `Error: Line 0 - Message\nStack Line 1\nStack Line 2`; - error.context = 'Line 1 - Context'; - error.help = 'Line 2 - Help'; - - const {stack} = utils.prepareStackForUser(error); - assert.equal(stack, `Error: Line 0 - Message\nLine 1 - Context\nLine 2 - Help\nStack Trace:\nStack Line 1\nStack Line 2`); + error.context = "Line 1 - Context"; + error.help = "Line 2 - Help"; + + const { stack } = utils.prepareStackForUser(error); + assert.equal( + stack, + `Error: Line 0 - Message\nLine 1 - Context\nLine 2 - Help\nStack Trace:\nStack Line 1\nStack Line 2`, + ); }); - it('removes the code stack in production mode, leaving just error message, context & help', function () { + it("removes the code stack in production mode, leaving just error message, context & help", function () { const originalMode = process.env.NODE_ENV; - process.env.NODE_ENV = 'production'; + process.env.NODE_ENV = "production"; - const error = new Error('Test') as Error & {context?: string; help?: string}; + const error = new Error("Test") as Error & { context?: string; help?: string }; error.stack = `Error: Line 0 - Message\nStack Line 1\nStack Line 2`; - error.context = 'Line 1 - Context'; - error.help = 'Line 2 - Help'; + error.context = "Line 1 - Context"; + error.help = "Line 2 - Help"; - const {stack} = utils.prepareStackForUser(error); + const { stack } = utils.prepareStackForUser(error); assert.equal(stack, `Error: Line 0 - Message\nLine 1 - Context\nLine 2 - Help`); process.env.NODE_ENV = originalMode; }); }); - describe('ErrorTypes', function () { - const expectations: Array<[GhostErrorConstructor, Partial & {message: string}]> = [ - [errors.InternalServerError, {statusCode: 500, level: 'critical', errorType: 'InternalServerError', message: 'The server has encountered an error.', hideStack: false}], - [errors.IncorrectUsageError, {statusCode: 400, level: 'critical', errorType: 'IncorrectUsageError', message: 'We detected a misuse. Please read the stack trace.', hideStack: false}], - [errors.NotFoundError, {statusCode: 404, level: 'normal', errorType: 'NotFoundError', message: 'Resource could not be found.', hideStack: true}], - [errors.BadRequestError, {statusCode: 400, level: 'normal', errorType: 'BadRequestError', message: 'The request could not be understood.', hideStack: false}], - [errors.UnauthorizedError, {statusCode: 401, level: 'normal', errorType: 'UnauthorizedError', message: 'You are not authorised to make this request.', hideStack: false}], - [errors.NoPermissionError, {statusCode: 403, level: 'normal', errorType: 'NoPermissionError', message: 'You do not have permission to perform this request.', hideStack: false}], - [errors.ValidationError, {statusCode: 422, level: 'normal', errorType: 'ValidationError', message: 'The request failed validation.', hideStack: false}], - [errors.UnsupportedMediaTypeError, {statusCode: 415, level: 'normal', errorType: 'UnsupportedMediaTypeError', message: 'The media in the request is not supported by the server.', hideStack: false}], - [errors.TooManyRequestsError, {statusCode: 429, level: 'normal', errorType: 'TooManyRequestsError', message: 'Server has received too many similar requests in a short space of time.', hideStack: false}], - [errors.MaintenanceError, {statusCode: 503, level: 'normal', errorType: 'MaintenanceError', message: 'The server is temporarily down for maintenance.', hideStack: false}], - [errors.MethodNotAllowedError, {statusCode: 405, level: 'normal', errorType: 'MethodNotAllowedError', message: 'Method not allowed for resource.', hideStack: false}], - [errors.RequestNotAcceptableError, {statusCode: 406, level: 'normal', errorType: 'RequestNotAcceptableError', message: 'Request not acceptable for provided Accept-Version header.', hideStack: true}], - [errors.RequestEntityTooLargeError, {statusCode: 413, level: 'normal', errorType: 'RequestEntityTooLargeError', message: 'Request was too big for the server to handle.', hideStack: false}], - [errors.RangeNotSatisfiableError, {statusCode: 416, level: 'normal', errorType: 'RangeNotSatisfiableError', message: 'Range not satisfiable for provided Range header.', hideStack: true}], - [errors.TokenRevocationError, {statusCode: 503, level: 'normal', errorType: 'TokenRevocationError', message: 'Token is no longer available.', hideStack: false}], - [errors.VersionMismatchError, {statusCode: 400, level: 'normal', errorType: 'VersionMismatchError', message: 'Requested version does not match server version.', hideStack: false}], - [errors.DataExportError, {statusCode: 500, level: 'normal', errorType: 'DataExportError', message: 'The server encountered an error whilst exporting data.', hideStack: false}], - [errors.DataImportError, {statusCode: 500, level: 'normal', errorType: 'DataImportError', message: 'The server encountered an error whilst importing data.', hideStack: false}], - [errors.EmailError, {statusCode: 500, level: 'normal', errorType: 'EmailError', message: 'The server encountered an error whilst sending email.', hideStack: false}], - [errors.ThemeValidationError, {statusCode: 422, level: 'normal', errorType: 'ThemeValidationError', message: 'The theme has a validation error.', hideStack: false}], - [errors.DisabledFeatureError, {statusCode: 409, level: 'normal', errorType: 'DisabledFeatureError', message: 'Unable to complete the request, this feature is disabled.', hideStack: false}], - [errors.UpdateCollisionError, {statusCode: 409, level: 'normal', errorType: 'UpdateCollisionError', message: 'Unable to complete the request, there was a conflict.', hideStack: false}], - [errors.HostLimitError, {statusCode: 403, level: 'normal', errorType: 'HostLimitError', message: 'Unable to complete the request, this resource is limited.', hideStack: true}], - [errors.HelperWarning, {statusCode: 400, level: 'normal', errorType: 'HelperWarning', message: 'A theme helper has done something unexpected.', hideStack: true}], - [errors.PasswordResetRequiredError, {statusCode: 401, level: 'normal', errorType: 'PasswordResetRequiredError', message: 'For security, you need to create a new password. An email has been sent to you with instructions!', hideStack: false}], - [errors.UnhandledJobError, {statusCode: 500, level: 'critical', errorType: 'UnhandledJobError', message: 'Processed job threw an unhandled error', hideStack: false}], - [errors.NoContentError, {statusCode: 204, level: 'normal', errorType: 'NoContentError', message: 'The server has encountered an error.', hideStack: true}], - [errors.ConflictError, {statusCode: 409, level: 'normal', errorType: 'ConflictError', message: 'The server has encountered an conflict.', hideStack: false}], - [errors.MigrationError, {statusCode: 500, level: 'critical', errorType: 'MigrationError', message: 'An error has occurred applying a database migration.', hideStack: false}] + describe("ErrorTypes", function () { + const expectations: Array< + [GhostErrorConstructor, Partial & { message: string }] + > = [ + [ + errors.InternalServerError, + { + statusCode: 500, + level: "critical", + errorType: "InternalServerError", + message: "The server has encountered an error.", + hideStack: false, + }, + ], + [ + errors.IncorrectUsageError, + { + statusCode: 400, + level: "critical", + errorType: "IncorrectUsageError", + message: "We detected a misuse. Please read the stack trace.", + hideStack: false, + }, + ], + [ + errors.NotFoundError, + { + statusCode: 404, + level: "normal", + errorType: "NotFoundError", + message: "Resource could not be found.", + hideStack: true, + }, + ], + [ + errors.BadRequestError, + { + statusCode: 400, + level: "normal", + errorType: "BadRequestError", + message: "The request could not be understood.", + hideStack: false, + }, + ], + [ + errors.UnauthorizedError, + { + statusCode: 401, + level: "normal", + errorType: "UnauthorizedError", + message: "You are not authorised to make this request.", + hideStack: false, + }, + ], + [ + errors.NoPermissionError, + { + statusCode: 403, + level: "normal", + errorType: "NoPermissionError", + message: "You do not have permission to perform this request.", + hideStack: false, + }, + ], + [ + errors.ValidationError, + { + statusCode: 422, + level: "normal", + errorType: "ValidationError", + message: "The request failed validation.", + hideStack: false, + }, + ], + [ + errors.UnsupportedMediaTypeError, + { + statusCode: 415, + level: "normal", + errorType: "UnsupportedMediaTypeError", + message: "The media in the request is not supported by the server.", + hideStack: false, + }, + ], + [ + errors.TooManyRequestsError, + { + statusCode: 429, + level: "normal", + errorType: "TooManyRequestsError", + message: + "Server has received too many similar requests in a short space of time.", + hideStack: false, + }, + ], + [ + errors.MaintenanceError, + { + statusCode: 503, + level: "normal", + errorType: "MaintenanceError", + message: "The server is temporarily down for maintenance.", + hideStack: false, + }, + ], + [ + errors.MethodNotAllowedError, + { + statusCode: 405, + level: "normal", + errorType: "MethodNotAllowedError", + message: "Method not allowed for resource.", + hideStack: false, + }, + ], + [ + errors.RequestNotAcceptableError, + { + statusCode: 406, + level: "normal", + errorType: "RequestNotAcceptableError", + message: "Request not acceptable for provided Accept-Version header.", + hideStack: true, + }, + ], + [ + errors.RequestEntityTooLargeError, + { + statusCode: 413, + level: "normal", + errorType: "RequestEntityTooLargeError", + message: "Request was too big for the server to handle.", + hideStack: false, + }, + ], + [ + errors.RangeNotSatisfiableError, + { + statusCode: 416, + level: "normal", + errorType: "RangeNotSatisfiableError", + message: "Range not satisfiable for provided Range header.", + hideStack: true, + }, + ], + [ + errors.TokenRevocationError, + { + statusCode: 503, + level: "normal", + errorType: "TokenRevocationError", + message: "Token is no longer available.", + hideStack: false, + }, + ], + [ + errors.VersionMismatchError, + { + statusCode: 400, + level: "normal", + errorType: "VersionMismatchError", + message: "Requested version does not match server version.", + hideStack: false, + }, + ], + [ + errors.DataExportError, + { + statusCode: 500, + level: "normal", + errorType: "DataExportError", + message: "The server encountered an error whilst exporting data.", + hideStack: false, + }, + ], + [ + errors.DataImportError, + { + statusCode: 500, + level: "normal", + errorType: "DataImportError", + message: "The server encountered an error whilst importing data.", + hideStack: false, + }, + ], + [ + errors.EmailError, + { + statusCode: 500, + level: "normal", + errorType: "EmailError", + message: "The server encountered an error whilst sending email.", + hideStack: false, + }, + ], + [ + errors.ThemeValidationError, + { + statusCode: 422, + level: "normal", + errorType: "ThemeValidationError", + message: "The theme has a validation error.", + hideStack: false, + }, + ], + [ + errors.DisabledFeatureError, + { + statusCode: 409, + level: "normal", + errorType: "DisabledFeatureError", + message: "Unable to complete the request, this feature is disabled.", + hideStack: false, + }, + ], + [ + errors.UpdateCollisionError, + { + statusCode: 409, + level: "normal", + errorType: "UpdateCollisionError", + message: "Unable to complete the request, there was a conflict.", + hideStack: false, + }, + ], + [ + errors.HostLimitError, + { + statusCode: 403, + level: "normal", + errorType: "HostLimitError", + message: "Unable to complete the request, this resource is limited.", + hideStack: true, + }, + ], + [ + errors.HelperWarning, + { + statusCode: 400, + level: "normal", + errorType: "HelperWarning", + message: "A theme helper has done something unexpected.", + hideStack: true, + }, + ], + [ + errors.PasswordResetRequiredError, + { + statusCode: 401, + level: "normal", + errorType: "PasswordResetRequiredError", + message: + "For security, you need to create a new password. An email has been sent to you with instructions!", + hideStack: false, + }, + ], + [ + errors.UnhandledJobError, + { + statusCode: 500, + level: "critical", + errorType: "UnhandledJobError", + message: "Processed job threw an unhandled error", + hideStack: false, + }, + ], + [ + errors.NoContentError, + { + statusCode: 204, + level: "normal", + errorType: "NoContentError", + message: "The server has encountered an error.", + hideStack: true, + }, + ], + [ + errors.ConflictError, + { + statusCode: 409, + level: "normal", + errorType: "ConflictError", + message: "The server has encountered an conflict.", + hideStack: false, + }, + ], + [ + errors.MigrationError, + { + statusCode: 500, + level: "critical", + errorType: "MigrationError", + message: "An error has occurred applying a database migration.", + hideStack: false, + }, + ], ]; for (const [ErrorClass, expected] of expectations) { @@ -383,7 +683,7 @@ describe('Errors', function () { assert.equal(error.hideStack, expected.hideStack); if (ErrorClass === errors.ThemeValidationError) { - assert.equal(typeof (error as any).errorDetails, 'object'); + assert.equal(typeof (error as any).errorDetails, "object"); } }); } diff --git a/packages/errors/test/utils.test.ts b/packages/errors/test/utils.test.ts index 47ad021df..a63a15fd6 100644 --- a/packages/errors/test/utils.test.ts +++ b/packages/errors/test/utils.test.ts @@ -1,28 +1,30 @@ -import assert from 'assert/strict'; +import assert from "assert/strict"; -import errors from '../src'; -import * as utils from '../src/utils'; +import errors from "../src"; +import * as utils from "../src/utils"; -describe('Error Utils', function () { - describe('prepareStackForUser', function () { - it('handles errors without stack', function () { - const error = new Error('no stack'); +describe("Error Utils", function () { + describe("prepareStackForUser", function () { + it("handles errors without stack", function () { + const error = new Error("no stack"); error.stack = undefined; const processedError = utils.prepareStackForUser(error); - assert.equal(processedError.stack, 'Stack Trace:'); + assert.equal(processedError.stack, "Stack Trace:"); }); - it('returns full error clone of nested errors', function () { - const originalError = new Error('I am the original one!') as Error & {custom?: string}; - originalError.custom = 'I am custom!'; + it("returns full error clone of nested errors", function () { + const originalError = new Error("I am the original one!") as Error & { + custom?: string; + }; + originalError.custom = "I am custom!"; const ghostError = new errors.ValidationError({ - message: 'mistakes were made', - help: 'help yourself', + message: "mistakes were made", + help: "help yourself", errorDetails: { - originalError: originalError - } + originalError: originalError, + }, }); const processedError = utils.prepareStackForUser(ghostError); @@ -34,15 +36,15 @@ describe('Error Utils', function () { assert.equal(processedError.errorDetails.originalError.message, originalError.message); assert.equal(processedError.errorDetails.originalError.custom, originalError.custom); - originalError.message = 'changed'; + originalError.message = "changed"; assert.notEqual(processedError.message, originalError.message); }); - it('deep clones array values in errorDetails', function () { - const items = [{id: 1}, {id: 2}]; + it("deep clones array values in errorDetails", function () { + const items = [{ id: 1 }, { id: 2 }]; const ghostError = new errors.ValidationError({ - message: 'mistakes were made', - errorDetails: items + message: "mistakes were made", + errorDetails: items, }); const processedError = utils.prepareStackForUser(ghostError); @@ -52,17 +54,17 @@ describe('Error Utils', function () { assert.equal(processedError.errorDetails[0].id, 1); }); - it('Preserves the stack trace', function () { + it("Preserves the stack trace", function () { const errorCreatingFunction = () => { - return new Error('Original error'); + return new Error("Original error"); }; const originalError = errorCreatingFunction(); const ghostError = new errors.EmailError({ - message: 'Ghost error', - err: originalError + message: "Ghost error", + err: originalError, }); - assert.equal(ghostError.stack!.includes('errorCreatingFunction'), true); + assert.equal(ghostError.stack!.includes("errorCreatingFunction"), true); }); }); }); diff --git a/packages/errors/test/wrap-stack.test.ts b/packages/errors/test/wrap-stack.test.ts index dd4ebe5d5..a2556243f 100644 --- a/packages/errors/test/wrap-stack.test.ts +++ b/packages/errors/test/wrap-stack.test.ts @@ -1,22 +1,25 @@ -import assert from 'assert/strict'; -import {wrapStack} from '../src/wrap-stack'; +import assert from "assert/strict"; +import { wrapStack } from "../src/wrap-stack"; -describe('wrapStack', function () { - it('returns combined stack lines', function () { - const ghostError = new Error('I am the ghost one!'); - ghostError.stack = 'ghost fn\nghost stack 1\nghost stack 2'; - const internalError = new Error('I am the internal one!'); - internalError.stack = 'internal fn\ninternal stack 1\ninternal stack 2'; +describe("wrapStack", function () { + it("returns combined stack lines", function () { + const ghostError = new Error("I am the ghost one!"); + ghostError.stack = "ghost fn\nghost stack 1\nghost stack 2"; + const internalError = new Error("I am the internal one!"); + internalError.stack = "internal fn\ninternal stack 1\ninternal stack 2"; - assert.equal(wrapStack(ghostError, internalError), 'internal fn\nghost stack 1\ninternal stack 1\ninternal stack 2'); + assert.equal( + wrapStack(ghostError, internalError), + "internal fn\nghost stack 1\ninternal stack 1\ninternal stack 2", + ); }); - it('handles errors without a stack', function () { - const ghostError = new Error('I am the ghost one!'); + it("handles errors without a stack", function () { + const ghostError = new Error("I am the ghost one!"); ghostError.stack = undefined; - const internalError = new Error('I am the internal one!'); + const internalError = new Error("I am the internal one!"); internalError.stack = undefined; - assert.equal(wrapStack(ghostError, internalError), '\n'); + assert.equal(wrapStack(ghostError, internalError), "\n"); }); }); diff --git a/packages/errors/tsconfig.json b/packages/errors/tsconfig.json index ea92ae75b..8888051e5 100644 --- a/packages/errors/tsconfig.json +++ b/packages/errors/tsconfig.json @@ -1,6 +1,4 @@ { "extends": "../tsconfig.json", - "include": [ - "src/**/*" - ] + "include": ["src/**/*"] } diff --git a/packages/errors/vitest.config.ts b/packages/errors/vitest.config.ts index 0a51d09c0..ddc255fc9 100644 --- a/packages/errors/vitest.config.ts +++ b/packages/errors/vitest.config.ts @@ -1,12 +1,15 @@ -import {defineConfig, mergeConfig} from 'vitest/config'; -import rootConfig from '../../vitest.config'; +import { defineConfig, mergeConfig } from "vitest/config"; +import rootConfig from "../../vitest.config"; // Override: TypeScript package with source in src/, not lib/. // Coverage must be scoped to src/ to measure the right files. -export default mergeConfig(rootConfig, defineConfig({ - test: { - coverage: { - include: ['src/**'] - } - } -})); +export default mergeConfig( + rootConfig, + defineConfig({ + test: { + coverage: { + include: ["src/**"], + }, + }, + }), +); diff --git a/packages/express-test/CLAUDE.md b/packages/express-test/CLAUDE.md index 4235d6462..afddac370 100644 --- a/packages/express-test/CLAUDE.md +++ b/packages/express-test/CLAUDE.md @@ -34,21 +34,21 @@ NODE_ENV=testing npx mocha --require ./test/utils/overrides.js './test/**/*.test ### Core Components 1. **Agent.js** (lib/Agent.js) - Main testing agent that wraps Express applications - - Handles cookie jar management - - Provides HTTP method shortcuts (get, post, put, delete, etc.) - - Integrates with snapshot testing + - Handles cookie jar management + - Provides HTTP method shortcuts (get, post, put, delete, etc.) + - Integrates with snapshot testing 2. **Request.js** (lib/Request.js) - Handles the actual request execution - - Manages request/response lifecycle - - Processes headers, body, and query parameters - - Executes the Express app internally without HTTP + - Manages request/response lifecycle + - Processes headers, body, and query parameters + - Executes the Express app internally without HTTP 3. **ExpectRequest.js** (lib/ExpectRequest.js) - Assertion chaining system - - Implements prioritized assertion execution order: - 1. `expect` (custom assertions) - 2. `expectHeader` (header assertions) - 3. `expectStatus` (status code assertions) - - Provides fluent API for test assertions + - Implements prioritized assertion execution order: + 1. `expect` (custom assertions) + 2. `expectHeader` (header assertions) + 3. `expectStatus` (status code assertions) + - Provides fluent API for test assertions ### Key Design Patterns @@ -69,4 +69,4 @@ NODE_ENV=testing npx mocha --require ./test/utils/overrides.js './test/**/*.test - **Debug Code Alert**: There's currently debug code in lib/Agent.js:25-26 that logs "IAM HERE" and throws an error. This needs to be removed before the code will function. - This is part of the Ghost Framework monorepo managed with Lerna - The package is publicly published to npm under @tryghost scope -- ESLint configuration extends Ghost's custom plugin rules \ No newline at end of file +- ESLint configuration extends Ghost's custom plugin rules diff --git a/packages/express-test/README.md b/packages/express-test/README.md index c4467f2d1..457d6b093 100644 --- a/packages/express-test/README.md +++ b/packages/express-test/README.md @@ -10,7 +10,6 @@ or `yarn add @tryghost/express-test` - ## Purpose High-level Express testing agent with chainable request/assertion APIs and snapshot helpers. @@ -26,7 +25,6 @@ For the most up-to-date and clear usage info, there's a live working example of An instantiated agent can make HTTP-like calls, with a supertest-like chained API to set headers & body and to check status, headers and anything else. - ``` const agent = new TestAgent(app) @@ -51,14 +49,15 @@ The agent maintains cookies across requests using a cookie jar. You can clear al agent.clearCookies(); // Both methods support chaining -await agent.logout().get('/protected-route'); +await agent.logout().get("/protected-route"); ``` This is an initial version for review. More docs coming if it works :) - ### Assertion execution order -The order of *chained* assertion execution is NOT the order they were declared in. The framework follows the priority order of the assertion types: + +The order of _chained_ assertion execution is NOT the order they were declared in. The framework follows the priority order of the assertion types: + 1. `expect` 2. `expectHeader` 3. `expectStatus` @@ -72,23 +71,19 @@ The custom order is here to prioritize assertions with the most context first. T This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests - - - # Copyright & License Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/express-test/example/app.js b/packages/express-test/example/app.js index e28263aa3..39c051e74 100644 --- a/packages/express-test/example/app.js +++ b/packages/express-test/example/app.js @@ -1,14 +1,14 @@ -const express = require('express'); -const session = require('express-session'); +const express = require("express"); +const session = require("express-session"); -const path = require('path'); -const fs = require('fs').promises; -const os = require('os'); -const multer = require('multer'); -const upload = multer({dest: os.tmpdir()}); +const path = require("path"); +const fs = require("fs").promises; +const os = require("os"); +const multer = require("multer"); +const upload = multer({ dest: os.tmpdir() }); const readJSONFile = async function (name) { - const data = await fs.readFile(path.join(__dirname, `${name}.json`), {encoding: 'utf8'}); + const data = await fs.readFile(path.join(__dirname, `${name}.json`), { encoding: "utf8" }); return JSON.parse(data); }; @@ -24,21 +24,23 @@ const isLoggedIn = function (req, res, next) { app.use(express.json()); -app.use(session({ - secret: 'verysecretstring', - name: 'testauth', - resave: false, - saveUninitialized: false -})); - -app.get('/', (req, res) => { - return res.send('Hello World!'); +app.use( + session({ + secret: "verysecretstring", + name: "testauth", + resave: false, + saveUninitialized: false, + }), +); + +app.get("/", (req, res) => { + return res.send("Hello World!"); }); /** An endpoint for checking headers and body gets sent */ -app.post('/check/', (req, res) => { - if (req.get('x-check')) { - res.set('x-checked', true); +app.post("/check/", (req, res) => { + if (req.get("x-check")) { + res.set("x-checked", true); } // express.json() ensures req.body is be an empty object @@ -49,10 +51,15 @@ app.post('/check/', (req, res) => { * API Methods */ -app.post('/api/session/', async (req, res) => { - const user = await readJSONFile('user'); +app.post("/api/session/", async (req, res) => { + const user = await readJSONFile("user"); - if (req.body.username && req.body.password && req.body.username === user.username && req.body.password === user.password) { + if ( + req.body.username && + req.body.password && + req.body.username === user.username && + req.body.password === user.password + ) { req.session.loggedIn = true; req.session.username = req.body.username; @@ -62,25 +69,29 @@ app.post('/api/session/', async (req, res) => { return res.sendStatus(401); }); -app.get('/api/foo/', isLoggedIn, async (req, res) => { - const data = await readJSONFile('data'); +app.get("/api/foo/", isLoggedIn, async (req, res) => { + const data = await readJSONFile("data"); return res.json(data); }); -app.post('/api/ping/', async (req, res) => { +app.post("/api/ping/", async (req, res) => { return res.json(req.body); }); -app.post('/api/upload/', upload.single('image'), async (req, res) => { +app.post("/api/upload/", upload.single("image"), async (req, res) => { return res.json(req.file); }); -app.post('/api/upload-multiple/', upload.fields([ - {name: 'image', maxCount: 1}, - {name: 'document', maxCount: 1} -]), async (req, res) => { - return res.json(req.files); -}); +app.post( + "/api/upload-multiple/", + upload.fields([ + { name: "image", maxCount: 1 }, + { name: "document", maxCount: 1 }, + ]), + async (req, res) => { + return res.json(req.files); + }, +); module.exports = app; diff --git a/packages/express-test/example/data.json b/packages/express-test/example/data.json index 44ee9d494..654791283 100644 --- a/packages/express-test/example/data.json +++ b/packages/express-test/example/data.json @@ -1,5 +1,7 @@ { - "foo": [{ + "foo": [ + { "bar": "baz" - }] + } + ] } diff --git a/packages/express-test/example/user.json b/packages/express-test/example/user.json index 4885aa9eb..5dbbc2ede 100644 --- a/packages/express-test/example/user.json +++ b/packages/express-test/example/user.json @@ -1 +1 @@ -{"username": "hello", "password": "world"} +{ "username": "hello", "password": "world" } diff --git a/packages/express-test/index.js b/packages/express-test/index.js index e4deb970b..f13e61cae 100644 --- a/packages/express-test/index.js +++ b/packages/express-test/index.js @@ -1,11 +1,11 @@ -module.exports = require('./lib/Agent'); +module.exports = require("./lib/Agent"); // NOTE: exposing jest-snapshot as a part of express-test to avoid // version mismatching on the client side module.exports.snapshot = { - mochaHooks: require('@tryghost/jest-snapshot').mochaHooks, - snapshotManager: require('@tryghost/jest-snapshot').snapshotManager, - matchSnapshotAssertion: require('@tryghost/jest-snapshot').matchSnapshotAssertion, - any: require('@tryghost/jest-snapshot').any, - stringMatching: require('@tryghost/jest-snapshot').stringMatching + mochaHooks: require("@tryghost/jest-snapshot").mochaHooks, + snapshotManager: require("@tryghost/jest-snapshot").snapshotManager, + matchSnapshotAssertion: require("@tryghost/jest-snapshot").matchSnapshotAssertion, + any: require("@tryghost/jest-snapshot").any, + stringMatching: require("@tryghost/jest-snapshot").stringMatching, }; diff --git a/packages/express-test/lib/Agent.js b/packages/express-test/lib/Agent.js index 114b82f91..a0679cba0 100644 --- a/packages/express-test/lib/Agent.js +++ b/packages/express-test/lib/Agent.js @@ -1,8 +1,8 @@ /* eslint-disable ghost/ghost-custom/no-native-error */ -const {CookieJar} = require('cookiejar'); -const ExpectRequest = require('./ExpectRequest'); -const {RequestOptions} = require('./Request'); -const {normalizeURL} = require('./utils'); +const { CookieJar } = require("cookiejar"); +const ExpectRequest = require("./ExpectRequest"); +const { RequestOptions } = require("./Request"); +const { normalizeURL } = require("./utils"); /** * @typedef AgentOptions @@ -39,7 +39,7 @@ class Agent { const baseUrl = urlOptions.baseUrl || this.defaults.baseUrl || null; if (baseUrl) { - processedURL = `/${baseUrl}/${processedURL}`.replace(/(^|[^:])\/\/+/g, '$1/'); + processedURL = `/${baseUrl}/${processedURL}`.replace(/(^|[^:])\/\/+/g, "$1/"); } processedURL = normalizeURL(processedURL); @@ -53,7 +53,7 @@ class Agent { searchParams.append(key, queryParams[key]); } - if (processedURL.includes('?')) { + if (processedURL.includes("?")) { processedURL = `${processedURL}&${searchParams.toString()}`; } else { processedURL = `${processedURL}?${searchParams.toString()}`; @@ -73,22 +73,26 @@ class Agent { _mergeOptions(method, url, options = {}) { // It doesn't make sense to call this method without these properties if (!method) { - throw new Error('_mergeOptions cannot be called without a method'); /* eslint-disable-line no-restricted-syntax */ + throw new Error( + "_mergeOptions cannot be called without a method", + ); /* eslint-disable-line no-restricted-syntax */ } if (!url) { - throw new Error('_mergeOptions cannot be called without a url'); /* eslint-disable-line no-restricted-syntax */ + throw new Error( + "_mergeOptions cannot be called without a url", + ); /* eslint-disable-line no-restricted-syntax */ } // urlOptions - const {baseUrl, queryParams} = options; + const { baseUrl, queryParams } = options; return new RequestOptions({ method, - url: this._makeUrl(url, Object.assign({}, {baseUrl, queryParams})), + url: this._makeUrl(url, Object.assign({}, { baseUrl, queryParams })), headers: Object.assign({}, this.defaults.headers, options.headers), // Set this to an empty object for ease, as express.json will do this anyway - body: Object.assign({}, this.defaults.body, options.body) + body: Object.assign({}, this.defaults.body, options.body), }); } @@ -102,12 +106,19 @@ class Agent { } } -['get', 'post', 'put', 'patch', 'delete', 'options', 'head'].forEach((method) => { +["get", "post", "put", "patch", "delete", "options", "head"].forEach((method) => { Agent.prototype[method] = function (url, options) { if (!url) { - throw new Error('Cannot make a request without supplying a url'); /* eslint-disable-line no-restricted-syntax */ + throw new Error( + "Cannot make a request without supplying a url", + ); /* eslint-disable-line no-restricted-syntax */ } - return new ExpectRequest(this.app, this.jar, this._mergeOptions(method.toUpperCase(), url, options), this.snapshotManager); + return new ExpectRequest( + this.app, + this.jar, + this._mergeOptions(method.toUpperCase(), url, options), + this.snapshotManager, + ); }; }); diff --git a/packages/express-test/lib/ExpectRequest.js b/packages/express-test/lib/ExpectRequest.js index 490b9b191..30a910beb 100644 --- a/packages/express-test/lib/ExpectRequest.js +++ b/packages/express-test/lib/ExpectRequest.js @@ -1,6 +1,6 @@ -const assert = require('assert'); -const {Request, RequestOptions} = require('./Request'); -const {snapshotManager} = require('@tryghost/jest-snapshot'); +const assert = require("assert"); +const { Request, RequestOptions } = require("./Request"); +const { snapshotManager } = require("@tryghost/jest-snapshot"); /** * @typedef {object} ExpressTestAssertion @@ -29,10 +29,10 @@ class ExpectRequest extends Request { } expect(callback) { - if (typeof callback !== 'function') { + if (typeof callback !== "function") { // eslint-disable-next-line ghost/ghost-custom/no-native-error throw new Error( - 'express-test expect() requires a callback function, did you mean expectStatus or expectHeader?' + "express-test expect() requires a callback function, did you mean expectStatus or expectHeader?", ); } @@ -47,7 +47,7 @@ class ExpectRequest extends Request { const assertion = { fn: wrapperFn, - type: 'body' + type: "body", }; this._addAssertion(assertion); @@ -59,7 +59,7 @@ class ExpectRequest extends Request { const assertion = { fn: this._assertStatus, expected, - type: 'status' + type: "status", }; this._addAssertion(assertion); @@ -72,7 +72,7 @@ class ExpectRequest extends Request { fn: this._assertHeader, expectedField: expectedField.toLowerCase(), expectedValue, - type: 'header' + type: "header", }; this._addAssertion(assertion); @@ -84,7 +84,7 @@ class ExpectRequest extends Request { const assertion = { fn: this._assertEmptyBody, expected: {}, - type: 'body' + type: "body", }; this._addAssertion(assertion); @@ -96,8 +96,8 @@ class ExpectRequest extends Request { let assertion = { fn: this._assertSnapshot, properties: properties, - field: 'body', - type: 'body' + field: "body", + type: "body", }; this._addAssertion(assertion); @@ -109,8 +109,8 @@ class ExpectRequest extends Request { let assertion = { fn: this._assertSnapshot, properties: properties, - field: 'headers', - type: 'header' + field: "headers", + type: "header", }; this._addAssertion(assertion); @@ -156,11 +156,11 @@ class ExpectRequest extends Request { const headerAssertions = []; for (const assertion of this.assertions) { - if (assertion.type === 'body') { + if (assertion.type === "body") { assertion.fn(response, assertion); - } else if (assertion.type === 'status') { + } else if (assertion.type === "status") { statusCodeAssertions.push(assertion); - } else if (assertion.type === 'header') { + } else if (assertion.type === "header") { headerAssertions.push(assertion); } else { headerAssertions.push(assertion); @@ -183,9 +183,9 @@ class ExpectRequest extends Request { _addAssertion(assertion) { // We create the error here so that we get a useful stack trace let error = new assert.AssertionError({ - message: 'Unexpected assertion error', + message: "Unexpected assertion error", expected: assertion.expected, - stackStartFn: this._addAssertion + stackStartFn: this._addAssertion, }); error.contextString = this.reqOptions.toString(); @@ -195,11 +195,16 @@ class ExpectRequest extends Request { } _assertStatus(response, assertion) { - const {error} = assertion; + const { error } = assertion; error.message = `Expected statusCode ${assertion.expected}, got statusCode ${response.statusCode} ${error.contextString}`; - if (response.body && response.body.errors && response.body.errors[0] && response.body.errors[0].message) { + if ( + response.body && + response.body.errors && + response.body.errors[0] && + response.body.errors[0].message + ) { error.message += `\n${response.body.errors[0].message}`; if (response.body.errors[0].context) { @@ -213,7 +218,7 @@ class ExpectRequest extends Request { } _assertHeader(response, assertion) { - const {expectedField, expectedValue, error} = assertion; + const { expectedField, expectedValue, error } = assertion; const actual = response.headers[expectedField]; const expectedHeaderString = `${expectedField}: ${expectedValue}`; @@ -237,7 +242,7 @@ class ExpectRequest extends Request { } _assertEmptyBody(response, assertion) { - const {error, expected} = assertion; + const { error, expected } = assertion; const actual = response.body; error.actual = actual; diff --git a/packages/express-test/lib/Request.js b/packages/express-test/lib/Request.js index ce05631c1..4ef924cac 100644 --- a/packages/express-test/lib/Request.js +++ b/packages/express-test/lib/Request.js @@ -1,14 +1,14 @@ -const http = require('http'); -const {Writable, Readable} = require('stream'); -const express = require('express'); -const {CookieAccessInfo} = require('cookiejar'); -const {parse} = require('url'); -const {isJSON, attachFile} = require('./utils'); +const http = require("http"); +const { Writable, Readable } = require("stream"); +const express = require("express"); +const { CookieAccessInfo } = require("cookiejar"); +const { parse } = require("url"); +const { isJSON, attachFile } = require("./utils"); class MockSocket extends Writable { constructor() { super(); - this.remoteAddress = '127.0.0.1'; + this.remoteAddress = "127.0.0.1"; } _write(chunk, encoding, callback) { callback(); @@ -16,9 +16,9 @@ class MockSocket extends Writable { } class RequestOptions { - constructor({method, url, headers, body} = {}) { - this.method = method || 'GET'; - this.url = url || '/'; + constructor({ method, url, headers, body } = {}) { + this.method = method || "GET"; + this.url = url || "/"; this.headers = headers || {}; this.body = body; } @@ -31,7 +31,8 @@ class RequestOptions { class Request { constructor(app, cookieJar, reqOptions) { this.app = app; - this.reqOptions = reqOptions instanceof RequestOptions ? reqOptions : new RequestOptions(reqOptions); + this.reqOptions = + reqOptions instanceof RequestOptions ? reqOptions : new RequestOptions(reqOptions); this.cookieJar = cookieJar; this._formData = null; // Track FormData instance for multiple attachments } @@ -49,21 +50,21 @@ class Request { * @returns */ body(body) { - if (body.getBuffer && typeof body.getBuffer === 'function') { + if (body.getBuffer && typeof body.getBuffer === "function") { // body is FormData (we cannot reliably check instanceof, not working in all situations) const requestBuffer = body.getBuffer(); const headers = body.getHeaders({ - ...this.reqOptions.headers + ...this.reqOptions.headers, }); this.reqOptions.headers = headers; return this.body(requestBuffer); } - if (typeof body === 'string') { - const buffer = Buffer.from(body, 'utf8'); + if (typeof body === "string") { + const buffer = Buffer.from(body, "utf8"); return this.body(buffer); } if (body instanceof Buffer) { - this.reqOptions.headers['content-length'] = body.length; + this.reqOptions.headers["content-length"] = body.length; return this.stream(Readable.from(body)); } @@ -110,7 +111,7 @@ class Request { }, read: () => { return readableStream.read(...arguments); - } + }, }; return this; } @@ -149,7 +150,7 @@ class Request { } _getReqRes() { - const {app, reqOptions} = this; + const { app, reqOptions } = this; // Create proper Node.js req/res objects using built-in http module. // MockSocket provides a writable stream so that res.end() properly emits 'finish'. @@ -159,7 +160,7 @@ class Request { // When streaming body data, the socket must appear readable so that // body-parser's on-finished check doesn't skip the request as "already finished". if (hasStreamOverrides) { - Object.defineProperty(socket, 'readable', {value: true}); + Object.defineProperty(socket, "readable", { value: true }); } const req = new http.IncomingMessage(socket); @@ -169,7 +170,7 @@ class Request { for (const key of Object.keys(reqOptions.headers)) { req.headers[key.toLowerCase()] = reqOptions.headers[key]; } - req.headers.host = req.headers.host || 'localhost'; + req.headers.host = req.headers.host || "localhost"; req.body = reqOptions.body; req.app = app; @@ -211,34 +212,37 @@ class Request { if (hasStreamOverrides) { const props = Object.keys(this.reqOptions.methodOverrides); for (const prop of props) { - const descriptor = Object.getOwnPropertyDescriptor(this.reqOptions.methodOverrides, prop); + const descriptor = Object.getOwnPropertyDescriptor( + this.reqOptions.methodOverrides, + prop, + ); Object.defineProperty(req, prop, descriptor); } } - return {req, res}; + return { req, res }; } _buildResponse(res) { const statusCode = res.statusCode; const headers = Object.assign({}, res.getHeaders()); - const text = res.body ? res.body.toString('utf8') : undefined; + const text = res.body ? res.body.toString("utf8") : undefined; let body = {}; - if (isJSON(res.getHeader('Content-Type'))) { + if (isJSON(res.getHeader("Content-Type"))) { body = text && JSON.parse(text); } - return {statusCode, headers, text, body, response: res}; + return { statusCode, headers, text, body, response: res }; } _doRequest(callback) { try { - const {req, res} = this._getReqRes(); + const { req, res } = this._getReqRes(); this._restoreCookies(req); - res.on('finish', () => { + res.on("finish", () => { const response = this._buildResponse(res); this._saveCookies(res); @@ -255,11 +259,7 @@ class Request { _getCookies(req) { const url = parse(req.url); - const access = new CookieAccessInfo( - url.hostname, - url.pathname, - url.protocol === 'https:' - ); + const access = new CookieAccessInfo(url.hostname, url.pathname, url.protocol === "https:"); return this.cookieJar.getCookies(access).toValueString(); } @@ -270,7 +270,7 @@ class Request { } _saveCookies(res) { - const cookies = res.getHeader('set-cookie'); + const cookies = res.getHeader("set-cookie"); if (cookies) { this.cookieJar.setCookies(cookies); diff --git a/packages/express-test/lib/utils.js b/packages/express-test/lib/utils.js index 65261e527..ae969f6d9 100644 --- a/packages/express-test/lib/utils.js +++ b/packages/express-test/lib/utils.js @@ -1,7 +1,7 @@ -const fs = require('fs'); -const path = require('path'); -const mime = require('mime-types'); -const FormData = require('form-data'); +const fs = require("fs"); +const path = require("path"); +const mime = require("mime-types"); +const FormData = require("form-data"); module.exports.isJSON = function isJSON(mimeType) { // should match /json or +json @@ -10,9 +10,9 @@ module.exports.isJSON = function isJSON(mimeType) { }; module.exports.normalizeURL = function normalizeURL(toNormalize) { - const split = toNormalize.split('?'); + const split = toNormalize.split("?"); const pathname = split[0]; - let normalized = pathname + (pathname.endsWith('/') ? '' : '/'); + let normalized = pathname + (pathname.endsWith("/") ? "" : "/"); if (split.length === 2) { normalized += `?${split[1]}`; @@ -25,11 +25,11 @@ module.exports.attachFile = function attachFile(name, filePath, existingFormData const formData = existingFormData || new FormData(); const fileContent = fs.readFileSync(filePath); const filename = path.basename(filePath); - const contentType = mime.lookup(filePath) || 'application/octet-stream'; + const contentType = mime.lookup(filePath) || "application/octet-stream"; formData.append(name, fileContent, { filename, - contentType + contentType, }); return formData; diff --git a/packages/express-test/package.json b/packages/express-test/package.json index 0028e8c18..965916e58 100644 --- a/packages/express-test/package.json +++ b/packages/express-test/package.json @@ -1,14 +1,21 @@ { "name": "@tryghost/express-test", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/express-test" }, - "author": "Ghost Foundation", - "license": "MIT", + "files": [ + "index.js", + "lib" + ], "main": "index.js", + "publishConfig": { + "access": "public" + }, "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage", @@ -16,12 +23,11 @@ "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, - "files": [ - "index.js", - "lib" - ], - "publishConfig": { - "access": "public" + "dependencies": { + "@tryghost/jest-snapshot": "^2.0.3", + "cookiejar": "2.1.4", + "form-data": "4.0.5", + "mime-types": "3.0.2" }, "devDependencies": { "express": "5.2.1", @@ -31,11 +37,5 @@ }, "peerDependencies": { "express": "^4.0.0 || ^5.0.0" - }, - "dependencies": { - "@tryghost/jest-snapshot": "^2.0.3", - "cookiejar": "2.1.4", - "form-data": "4.0.5", - "mime-types": "3.0.2" } } diff --git a/packages/express-test/test/.eslintrc.js b/packages/express-test/test/.eslintrc.js index ef13dee0e..c9b01755e 100644 --- a/packages/express-test/test/.eslintrc.js +++ b/packages/express-test/test/.eslintrc.js @@ -1,9 +1,7 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ], + plugins: ["ghost"], + extends: ["plugin:ghost/test"], globals: { - beforeAll: 'readonly' - } + beforeAll: "readonly", + }, }; diff --git a/packages/express-test/test/Agent.test.js b/packages/express-test/test/Agent.test.js index ae57a31e4..99fb2dd47 100644 --- a/packages/express-test/test/Agent.test.js +++ b/packages/express-test/test/Agent.test.js @@ -1,128 +1,128 @@ -const {assert} = require('./utils'); +const { assert } = require("./utils"); -const Agent = require('../lib/Agent'); +const Agent = require("../lib/Agent"); -describe('Agent', function () { - describe('Class methods', function () { - it('constructor sets app + defaults, & creates http verb methods', function () { - const fn = () => { }; +describe("Agent", function () { + describe("Class methods", function () { + it("constructor sets app + defaults, & creates http verb methods", function () { + const fn = () => {}; const opts = {}; const agent = new Agent(fn, opts); assert.equal(agent.app, fn); assert.equal(agent.defaults, opts); - assert.equal(typeof agent.get, 'function'); - assert.equal(typeof agent.post, 'function'); - assert.equal(typeof agent.put, 'function'); - assert.equal(typeof agent.patch, 'function'); - assert.equal(typeof agent.delete, 'function'); - assert.equal(typeof agent.options, 'function'); - assert.equal(typeof agent.head, 'function'); + assert.equal(typeof agent.get, "function"); + assert.equal(typeof agent.post, "function"); + assert.equal(typeof agent.put, "function"); + assert.equal(typeof agent.patch, "function"); + assert.equal(typeof agent.delete, "function"); + assert.equal(typeof agent.options, "function"); + assert.equal(typeof agent.head, "function"); }); - it('constructor works without being passed defaults', function () { - const fn = () => { }; + it("constructor works without being passed defaults", function () { + const fn = () => {}; const agent = new Agent(fn); assert.equal(agent.app, fn); assert.deepEqual(agent.defaults, {}); - assert.equal(typeof agent.get, 'function'); - assert.equal(typeof agent.post, 'function'); - assert.equal(typeof agent.put, 'function'); - assert.equal(typeof agent.patch, 'function'); - assert.equal(typeof agent.delete, 'function'); - assert.equal(typeof agent.options, 'function'); - assert.equal(typeof agent.head, 'function'); + assert.equal(typeof agent.get, "function"); + assert.equal(typeof agent.post, "function"); + assert.equal(typeof agent.put, "function"); + assert.equal(typeof agent.patch, "function"); + assert.equal(typeof agent.delete, "function"); + assert.equal(typeof agent.options, "function"); + assert.equal(typeof agent.head, "function"); }); - it('_makeUrl without defaults', function () { - const fn = () => { }; + it("_makeUrl without defaults", function () { + const fn = () => {}; const opts = {}; const agent = new Agent(fn, opts); - assert.equal(agent._makeUrl('/'), '/'); + assert.equal(agent._makeUrl("/"), "/"); }); - it('_makeUrl with baseUrl + slashes', function () { - const fn = () => { }; + it("_makeUrl with baseUrl + slashes", function () { + const fn = () => {}; const opts = { - baseUrl: '/base/' + baseUrl: "/base/", }; const agent = new Agent(fn, opts); - assert.equal(agent._makeUrl('/'), '/base/'); + assert.equal(agent._makeUrl("/"), "/base/"); }); - it('_makeUrl with baseUrl + no slashes', function () { - const fn = () => { }; + it("_makeUrl with baseUrl + no slashes", function () { + const fn = () => {}; const opts = { - baseUrl: 'base' + baseUrl: "base", }; const agent = new Agent(fn, opts); - assert.equal(agent._makeUrl('/'), '/base/'); + assert.equal(agent._makeUrl("/"), "/base/"); }); - it('_makeUrl with baseUrl + override baseUrl', function () { - const fn = () => { }; + it("_makeUrl with baseUrl + override baseUrl", function () { + const fn = () => {}; const opts = { - baseUrl: '/base/' + baseUrl: "/base/", }; const agent = new Agent(fn, opts); - assert.equal(agent._makeUrl('/', {baseUrl: 'override'}), '/override/'); + assert.equal(agent._makeUrl("/", { baseUrl: "override" }), "/override/"); }); - it('_makeUrl with query params', function () { - const fn = () => { }; + it("_makeUrl with query params", function () { + const fn = () => {}; const opts = { queryParams: { - key: 'very_secret' - } + key: "very_secret", + }, }; const agent = new Agent(fn, opts); - assert.equal(agent._makeUrl('/'), '/?key=very_secret'); + assert.equal(agent._makeUrl("/"), "/?key=very_secret"); }); - it('_makeUrl with queryParams + override queryParams', function () { - const fn = () => { }; + it("_makeUrl with queryParams + override queryParams", function () { + const fn = () => {}; const opts = { queryParams: { - key: 'very_secret', - foo: 'bar' - } + key: "very_secret", + foo: "bar", + }, }; const overrides = { queryParams: { - key: 'not_so_secret', - bar: 'baz' - } + key: "not_so_secret", + bar: "baz", + }, }; const agent = new Agent(fn, opts); - assert.equal(agent._makeUrl('/', overrides), '/?key=not_so_secret&foo=bar&bar=baz'); + assert.equal(agent._makeUrl("/", overrides), "/?key=not_so_secret&foo=bar&bar=baz"); }); - it('_makeUrl with query params and existing query params', function () { - const fn = () => { }; + it("_makeUrl with query params and existing query params", function () { + const fn = () => {}; const opts = { queryParams: { - key: 'very_secret' - } + key: "very_secret", + }, }; const agent = new Agent(fn, opts); - assert.equal(agent._makeUrl('/?hello=world'), '/?hello=world&key=very_secret'); + assert.equal(agent._makeUrl("/?hello=world"), "/?hello=world&key=very_secret"); }); - it('_mergeOptions validation', function () { - const fn = () => { }; + it("_mergeOptions validation", function () { + const fn = () => {}; const opts = { - baseUrl: 'base' + baseUrl: "base", }; const agent = new Agent(fn, opts); @@ -131,11 +131,11 @@ describe('Agent', function () { }; const methodOnly = () => { - agent._mergeOptions('GET'); + agent._mergeOptions("GET"); }; const valid = () => { - agent._mergeOptions('GET', '/'); + agent._mergeOptions("GET", "/"); }; assert.throws(noOptions); @@ -143,70 +143,73 @@ describe('Agent', function () { assert.doesNotThrow(valid); }); - it('_mergeOptions with no defaults or options', function () { - const fn = () => { }; + it("_mergeOptions with no defaults or options", function () { + const fn = () => {}; const opts = {}; const agent = new Agent(fn, opts); - const options = agent._mergeOptions('GET', '/'); - assert.equal(options.method, 'GET'); - assert.equal(options.url, '/'); + const options = agent._mergeOptions("GET", "/"); + assert.equal(options.method, "GET"); + assert.equal(options.url, "/"); assert.deepEqual(options.headers, {}); assert.deepEqual(options.body, {}); }); - it('_mergeOptions with defaults but no options', function () { - const fn = () => { }; + it("_mergeOptions with defaults but no options", function () { + const fn = () => {}; const opts = { - baseUrl: 'base', - headers: {origin: 'localhost'}, - body: {hello: 'world'} + baseUrl: "base", + headers: { origin: "localhost" }, + body: { hello: "world" }, }; const agent = new Agent(fn, opts); - const options = agent._mergeOptions('GET', '/', {}); - assert.equal(options.method, 'GET'); - assert.equal(options.url, '/base/'); - assert.deepEqual(options.headers, {origin: 'localhost'}); - assert.deepEqual(options.body, {hello: 'world'}); + const options = agent._mergeOptions("GET", "/", {}); + assert.equal(options.method, "GET"); + assert.equal(options.url, "/base/"); + assert.deepEqual(options.headers, { origin: "localhost" }); + assert.deepEqual(options.body, { hello: "world" }); }); - it('_mergeOptions with no defaults but all options', function () { - const fn = () => { }; + it("_mergeOptions with no defaults but all options", function () { + const fn = () => {}; const opts = {}; const agent = new Agent(fn, opts); - const options = agent._mergeOptions('GET', '/', { - headers: {'content-type': 'application/json'}, - body: {foo: 'bar'} + const options = agent._mergeOptions("GET", "/", { + headers: { "content-type": "application/json" }, + body: { foo: "bar" }, }); - assert.equal(options.method, 'GET'); - assert.equal(options.url, '/'); - assert.deepEqual(options.headers, {'content-type': 'application/json'}); - assert.deepEqual(options.body, {foo: 'bar'}); + assert.equal(options.method, "GET"); + assert.equal(options.url, "/"); + assert.deepEqual(options.headers, { "content-type": "application/json" }); + assert.deepEqual(options.body, { foo: "bar" }); }); - it('_mergeOptions with defaults and options', function () { - const fn = () => { }; + it("_mergeOptions with defaults and options", function () { + const fn = () => {}; const opts = { - baseUrl: 'base', - headers: {origin: 'localhost'}, - body: {hello: 'world'} + baseUrl: "base", + headers: { origin: "localhost" }, + body: { hello: "world" }, }; const agent = new Agent(fn, opts); - const options = agent._mergeOptions('GET', '/', { - headers: {'content-type': 'application/json'}, - body: {foo: 'bar'} + const options = agent._mergeOptions("GET", "/", { + headers: { "content-type": "application/json" }, + body: { foo: "bar" }, }); - assert.equal(options.method, 'GET'); - assert.equal(options.url, '/base/'); - assert.deepEqual(options.headers, {origin: 'localhost', 'content-type': 'application/json'}); - assert.deepEqual(options.body, {hello: 'world', foo: 'bar'}); + assert.equal(options.method, "GET"); + assert.equal(options.url, "/base/"); + assert.deepEqual(options.headers, { + origin: "localhost", + "content-type": "application/json", + }); + assert.deepEqual(options.body, { hello: "world", foo: "bar" }); }); - it('http verb methods error without url', function () { - const fn = () => { }; + it("http verb methods error without url", function () { + const fn = () => {}; const opts = {}; const agent = new Agent(fn, opts); @@ -214,48 +217,52 @@ describe('Agent', function () { agent.get(); }; - assert.throws(noUrl, {message: 'Cannot make a request without supplying a url'}); + assert.throws(noUrl, { message: "Cannot make a request without supplying a url" }); }); - it('http verb methods (public interface)', function () { - const fn = () => { }; + it("http verb methods (public interface)", function () { + const fn = () => {}; const opts = {}; const agent = new Agent(fn, opts); - const test = agent.get('/'); + const test = agent.get("/"); - assert.equal(test instanceof require('../lib/ExpectRequest'), true); - assert.equal(test instanceof require('../lib/Request'), true); + assert.equal(test instanceof require("../lib/ExpectRequest"), true); + assert.equal(test instanceof require("../lib/Request"), true); assert.equal(test.app, fn); - assert.equal(test.reqOptions.method, 'GET'); - assert.equal(test.reqOptions.url, '/'); + assert.equal(test.reqOptions.method, "GET"); + assert.equal(test.reqOptions.url, "/"); assert.deepEqual(test.reqOptions.headers, {}); assert.deepEqual(test.reqOptions.body, {}); }); - it('clearCookies creates a new CookieJar and returns agent for chaining', function () { - const fn = () => { }; + it("clearCookies creates a new CookieJar and returns agent for chaining", function () { + const fn = () => {}; const opts = {}; const agent = new Agent(fn, opts); const originalJar = agent.jar; const result = agent.clearCookies(); - assert.notEqual(agent.jar, originalJar, 'Should create a new CookieJar instance'); - assert.equal(result, agent, 'Should return agent for chaining'); - assert.equal(typeof agent.jar.setCookie, 'function', 'New jar should be a valid CookieJar'); + assert.notEqual(agent.jar, originalJar, "Should create a new CookieJar instance"); + assert.equal(result, agent, "Should return agent for chaining"); + assert.equal( + typeof agent.jar.setCookie, + "function", + "New jar should be a valid CookieJar", + ); }); - it('clearCookies can be chained with other methods', function () { - const fn = () => { }; + it("clearCookies can be chained with other methods", function () { + const fn = () => {}; const opts = {}; const agent = new Agent(fn, opts); - const request = agent.clearCookies().get('/'); + const request = agent.clearCookies().get("/"); - assert.equal(request instanceof require('../lib/ExpectRequest'), true); - assert.equal(request.reqOptions.method, 'GET'); - assert.equal(request.reqOptions.url, '/'); + assert.equal(request instanceof require("../lib/ExpectRequest"), true); + assert.equal(request.reqOptions.method, "GET"); + assert.equal(request.reqOptions.url, "/"); }); }); }); diff --git a/packages/express-test/test/ExpectRequest.test.js b/packages/express-test/test/ExpectRequest.test.js index ac75564ad..e68193c47 100644 --- a/packages/express-test/test/ExpectRequest.test.js +++ b/packages/express-test/test/ExpectRequest.test.js @@ -1,17 +1,17 @@ -const {assert, sinon, stubCookies} = require('./utils'); +const { assert, sinon, stubCookies } = require("./utils"); -const {ExpectRequest, RequestOptions} = require('../lib/ExpectRequest'); -const {snapshotManager} = require('@tryghost/jest-snapshot'); -const Request = require('../lib/Request'); +const { ExpectRequest, RequestOptions } = require("../lib/ExpectRequest"); +const { snapshotManager } = require("@tryghost/jest-snapshot"); +const Request = require("../lib/Request"); -describe('ExpectRequest', function () { +describe("ExpectRequest", function () { afterEach(function () { sinon.restore(); }); - describe('Class functions', function () { - it('constructor sets app, jar and reqOptions', function () { - const fn = () => { }; + describe("Class functions", function () { + it("constructor sets app, jar and reqOptions", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new ExpectRequest(fn, jar, opts); @@ -21,10 +21,10 @@ describe('ExpectRequest', function () { assert.equal(request.reqOptions, opts); }); - it('class is thenable [public api]', async function () { + it("class is thenable [public api]", async function () { const fn = (req, res) => { // This is how reqresnext works - res.emit('finish'); + res.emit("finish"); }; const jar = {}; const opts = new RequestOptions(); @@ -37,15 +37,17 @@ describe('ExpectRequest', function () { const response = await request; assert.equal(response.statusCode, 200); // this is the default } catch (error) { - assert.fail(`This should not have thrown an error. Original error: ${error.message}.`); + assert.fail( + `This should not have thrown an error. Original error: ${error.message}.`, + ); } }); - it('finalize with no assertions doesnt try to run assertions', async function () { + it("finalize with no assertions doesnt try to run assertions", async function () { await new Promise((resolve, reject) => { const fn = (req, res) => { // This is how reqresnext works - res.emit('finish'); + res.emit("finish"); }; const jar = {}; const opts = new RequestOptions(); @@ -54,8 +56,8 @@ describe('ExpectRequest', function () { stubCookies(request); try { - const superStub = sinon.stub(Request.prototype, 'finalize').callsArg(0); - const assertSpy = sinon.stub(request, '_assertAll'); + const superStub = sinon.stub(Request.prototype, "finalize").callsArg(0); + const assertSpy = sinon.stub(request, "_assertAll"); // I couldn't figure out how to stub the super.finalize call here request.finalize((error) => { @@ -75,11 +77,11 @@ describe('ExpectRequest', function () { }); }); - it('finalize with assertions runs assertions', async function () { + it("finalize with assertions runs assertions", async function () { await new Promise((resolve, reject) => { const fn = (req, res) => { // This is how reqresnext works - res.emit('finish'); + res.emit("finish"); }; const jar = {}; const opts = new RequestOptions(); @@ -90,8 +92,8 @@ describe('ExpectRequest', function () { stubCookies(request); try { - const superStub = sinon.stub(Request.prototype, 'finalize').callsArg(0); - const assertSpy = sinon.stub(request, '_assertAll'); + const superStub = sinon.stub(Request.prototype, "finalize").callsArg(0); + const assertSpy = sinon.stub(request, "_assertAll"); request.finalize((error) => { if (error) { @@ -110,11 +112,11 @@ describe('ExpectRequest', function () { }); }); - it('finalize errors correctly when super.finalize is erroring', async function () { + it("finalize errors correctly when super.finalize is erroring", async function () { await new Promise((resolve, reject) => { const fn = (req, res) => { // This is how reqresnext works - res.emit('finish'); + res.emit("finish"); }; const jar = {}; const opts = new RequestOptions(); @@ -127,8 +129,10 @@ describe('ExpectRequest', function () { const theError = new Error(); try { - const superStub = sinon.stub(Request.prototype, 'finalize').callsArgWith(0, theError); - const assertSpy = sinon.stub(request, '_assertAll'); + const superStub = sinon + .stub(Request.prototype, "finalize") + .callsArgWith(0, theError); + const assertSpy = sinon.stub(request, "_assertAll"); request.finalize((error) => { sinon.assert.calledOnce(superStub); @@ -142,11 +146,11 @@ describe('ExpectRequest', function () { }); }); - it('finalize errors correctly when assertions are erroring', async function () { + it("finalize errors correctly when assertions are erroring", async function () { await new Promise((resolve, reject) => { const fn = (req, res) => { // This is how reqresnext works - res.emit('finish'); + res.emit("finish"); }; const jar = {}; const opts = new RequestOptions(); @@ -159,8 +163,8 @@ describe('ExpectRequest', function () { const theError = new Error(); try { - const superStub = sinon.stub(Request.prototype, 'finalize').callsArg(0); - const assertSpy = sinon.stub(request, '_assertAll').throws(theError); + const superStub = sinon.stub(Request.prototype, "finalize").callsArg(0); + const assertSpy = sinon.stub(request, "_assertAll").throws(theError); request.finalize((error) => { sinon.assert.calledOnce(superStub); @@ -174,8 +178,8 @@ describe('ExpectRequest', function () { }); }); - it('_addAssertion adds an assertion', function () { - const fn = () => { }; + it("_addAssertion adds an assertion", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new ExpectRequest(fn, jar, opts); @@ -187,12 +191,12 @@ describe('ExpectRequest', function () { const added = request.assertions[0]; assert.notEqual(request.assertions.length, 0); - assert.equal(added.error.message, 'Unexpected assertion error'); - assert.equal(added.error.contextString, 'GET request on /'); + assert.equal(added.error.message, "Unexpected assertion error"); + assert.equal(added.error.contextString, "GET request on /"); }); - it('_assertAll calls assertion functions for each assertion', function () { - const fn = () => { }; + it("_assertAll calls assertion functions for each assertion", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new ExpectRequest(fn, jar, opts); @@ -201,14 +205,14 @@ describe('ExpectRequest', function () { request.assertions = [ { - fn: fakeAssertion + fn: fakeAssertion, }, { - fn: fakeAssertion - } + fn: fakeAssertion, + }, ]; - const response = {foo: 'bar'}; + const response = { foo: "bar" }; request._assertAll(response); @@ -217,7 +221,7 @@ describe('ExpectRequest', function () { }); it('_assertAll calls assertion functions in order of "no type", "header" type, and "status" type', function () { - const fn = () => { }; + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new ExpectRequest(fn, jar, opts); @@ -228,19 +232,19 @@ describe('ExpectRequest', function () { request.assertions = [ { - type: 'status', - fn: statusSpy + type: "status", + fn: statusSpy, }, { - fn: noTypeSpy + fn: noTypeSpy, }, { - type: 'header', - fn: headerSpy - } + type: "header", + fn: headerSpy, + }, ]; - const response = {foo: 'bar'}; + const response = { foo: "bar" }; request._assertAll(response); @@ -256,17 +260,17 @@ describe('ExpectRequest', function () { sinon.assert.callOrder(noTypeSpy, headerSpy, statusSpy); }); - it('_assertStatus ok when status is ok', function () { - const fn = () => { }; + it("_assertStatus ok when status is ok", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new ExpectRequest(fn, jar, opts); const error = new assert.AssertionError({}); - error.contextString = 'foo'; + error.contextString = "foo"; - const response = {statusCode: 200}; - const assertion = {expected: 200, error}; + const response = { statusCode: 200 }; + const assertion = { expected: 200, error }; const assertFn = () => { request._assertStatus(response, assertion); @@ -275,17 +279,17 @@ describe('ExpectRequest', function () { assert.doesNotThrow(assertFn); }); - it('_assertStatus not ok when status is not ok', function () { - const fn = () => { }; + it("_assertStatus not ok when status is not ok", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new ExpectRequest(fn, jar, opts); const error = new assert.AssertionError({}); - error.contextString = 'foo'; + error.contextString = "foo"; - const response = {statusCode: 404}; - const assertion = {expected: 200, error}; + const response = { statusCode: 404 }; + const assertion = { expected: 200, error }; const assertFn = () => { request._assertStatus(response, assertion); @@ -294,70 +298,79 @@ describe('ExpectRequest', function () { assert.throws(assertFn); }); - it('_assertStatus not ok when status i not ok and shows response error when present', function () { - const fn = () => { }; + it("_assertStatus not ok when status i not ok and shows response error when present", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new ExpectRequest(fn, jar, opts); const error = new assert.AssertionError({}); - error.contextString = 'foo'; + error.contextString = "foo"; const response = { statusCode: 404, body: { - errors: [{ - message: 'Not found' - }] - } + errors: [ + { + message: "Not found", + }, + ], + }, }; - const assertion = {expected: 200, error}; + const assertion = { expected: 200, error }; const assertFn = () => { request._assertStatus(response, assertion); }; - assert.throws(assertFn, {message: 'Expected statusCode 200, got statusCode 404 foo\nNot found'}); + assert.throws(assertFn, { + message: "Expected statusCode 200, got statusCode 404 foo\nNot found", + }); }); - it('_assertStatus not ok when status i not ok and shows response context when present', function () { - const fn = () => { }; + it("_assertStatus not ok when status i not ok and shows response context when present", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new ExpectRequest(fn, jar, opts); const error = new assert.AssertionError({}); - error.contextString = 'foo'; + error.contextString = "foo"; const response = { statusCode: 500, body: { - errors: [{ - message: 'Internal server error, cannot save member.', - context: 'offer is not defined on the model.' - }] - } + errors: [ + { + message: "Internal server error, cannot save member.", + context: "offer is not defined on the model.", + }, + ], + }, }; - const assertion = {expected: 200, error}; + const assertion = { expected: 200, error }; const assertFn = () => { request._assertStatus(response, assertion); }; - assert.throws(assertFn, {message: 'Expected statusCode 200, got statusCode 500 foo\nInternal server error, cannot save member.\noffer is not defined on the model.'}); + assert.throws(assertFn, { + message: + "Expected statusCode 200, got statusCode 500 foo\nInternal server error, cannot save member.\noffer is not defined on the model.", + }); }); - it('_assertHeader ok when header is ok', function () { - const fn = () => { }; + it("_assertHeader ok when header is ok", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new ExpectRequest(fn, jar, opts); const error = new assert.AssertionError({}); - error.contextString = 'foo'; + error.contextString = "foo"; - const response = {headers: {foo: 'bar'}}; - const assertion = {expectedField: 'foo', expectedValue: 'bar', error}; + const response = { headers: { foo: "bar" } }; + const assertion = { expectedField: "foo", expectedValue: "bar", error }; const assertFn = () => { request._assertHeader(response, assertion); @@ -366,55 +379,57 @@ describe('ExpectRequest', function () { assert.doesNotThrow(assertFn); }); - it('_assertHeader not ok when header is not ok', function () { - const fn = () => { }; + it("_assertHeader not ok when header is not ok", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new ExpectRequest(fn, jar, opts); const error = new assert.AssertionError({}); - error.contextString = 'foo'; + error.contextString = "foo"; - const response = {headers: {foo: 'baz'}}; - const assertion = {expectedField: 'foo', expectedValue: 'bar', error}; + const response = { headers: { foo: "baz" } }; + const assertion = { expectedField: "foo", expectedValue: "bar", error }; const assertFn = () => { request._assertHeader(response, assertion); }; - assert.throws(assertFn, {message: 'Expected header "foo: bar", got "foo: baz" foo'}); + assert.throws(assertFn, { message: 'Expected header "foo: bar", got "foo: baz" foo' }); }); - it('_assertHeader not ok when status is not set', function () { - const fn = () => { }; + it("_assertHeader not ok when status is not set", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new ExpectRequest(fn, jar, opts); const error = new assert.AssertionError({}); - error.contextString = 'foo'; + error.contextString = "foo"; - const response = {headers: {}}; - const assertion = {expectedField: 'foo', expectedValue: 'bar', error}; + const response = { headers: {} }; + const assertion = { expectedField: "foo", expectedValue: "bar", error }; const assertFn = () => { request._assertHeader(response, assertion); }; - assert.throws(assertFn, {message: 'Expected header "foo: bar" to exist, got headers: {} foo'}); + assert.throws(assertFn, { + message: 'Expected header "foo: bar" to exist, got headers: {} foo', + }); }); - it('_assertHeader ok with matching regex for value', function () { - const fn = () => { }; + it("_assertHeader ok with matching regex for value", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new ExpectRequest(fn, jar, opts); const error = new assert.AssertionError({}); - error.contextString = 'foo'; + error.contextString = "foo"; - const response = {headers: {foo: 'baz'}}; - const assertion = {expectedField: 'foo', expectedValue: /^ba/, error}; + const response = { headers: { foo: "baz" } }; + const assertion = { expectedField: "foo", expectedValue: /^ba/, error }; const assertFn = () => { request._assertHeader(response, assertion); @@ -423,38 +438,40 @@ describe('ExpectRequest', function () { assert.doesNotThrow(assertFn); }); - it('_assertHeader mot ok with non-matching regex for value', function () { - const fn = () => { }; + it("_assertHeader mot ok with non-matching regex for value", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new ExpectRequest(fn, jar, opts); const error = new assert.AssertionError({}); - error.contextString = 'foo'; + error.contextString = "foo"; - const response = {headers: {foo: 'baz'}}; - const assertion = {expectedField: 'foo', expectedValue: /^bar/, error}; + const response = { headers: { foo: "baz" } }; + const assertion = { expectedField: "foo", expectedValue: /^bar/, error }; const assertFn = () => { request._assertHeader(response, assertion); }; - assert.throws(assertFn, {message: 'Expected header "foo" to have value matching "/^bar/", got "baz" foo'}); + assert.throws(assertFn, { + message: 'Expected header "foo" to have value matching "/^bar/", got "baz" foo', + }); }); - it('_assertSnapshot ok when match is a pass', function () { - const fn = () => { }; + it("_assertSnapshot ok when match is a pass", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new ExpectRequest(fn, jar, opts); const error = new assert.AssertionError({}); - error.contextString = 'foo'; + error.contextString = "foo"; - const response = {body: {foo: 'bar'}}; - const assertion = {properties: {}, field: 'body', error}; + const response = { body: { foo: "bar" } }; + const assertion = { properties: {}, field: "body", error }; - const matchStub = sinon.stub(snapshotManager, 'match').returns({pass: true}); + const matchStub = sinon.stub(snapshotManager, "match").returns({ pass: true }); const assertFn = () => { request._assertSnapshot(response, assertion); @@ -464,22 +481,22 @@ describe('ExpectRequest', function () { // Assert side effects, check that hinting works as expected sinon.assert.calledOnce(matchStub); - sinon.assert.calledOnceWithExactly(matchStub, response.body, {}, '[body]'); + sinon.assert.calledOnceWithExactly(matchStub, response.body, {}, "[body]"); }); - it('_assertSnapshot not ok when match is not a pass', function () { - const fn = () => { }; + it("_assertSnapshot not ok when match is not a pass", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new ExpectRequest(fn, jar, opts); const error = new assert.AssertionError({}); - error.contextString = 'foo'; + error.contextString = "foo"; - const response = {body: {foo: 'bar'}}; - const assertion = {properties: {}, field: 'body', error}; + const response = { body: { foo: "bar" } }; + const assertion = { properties: {}, field: "body", error }; - sinon.stub(snapshotManager, 'match').returns({pass: false}); + sinon.stub(snapshotManager, "match").returns({ pass: false }); const assertFn = () => { request._assertSnapshot(response, assertion); @@ -488,68 +505,73 @@ describe('ExpectRequest', function () { assert.throws(assertFn); }); - it('_assertSnapshot not ok when field not set', function () { - const fn = () => { }; + it("_assertSnapshot not ok when field not set", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new ExpectRequest(fn, jar, opts); const error = new assert.AssertionError({}); - error.contextString = 'foo'; + error.contextString = "foo"; - const response = {body: {foo: 'bar'}}; - const assertion = {properties: {}, error}; + const response = { body: { foo: "bar" } }; + const assertion = { properties: {}, error }; - sinon.stub(snapshotManager, 'match').returns({pass: false}); + sinon.stub(snapshotManager, "match").returns({ pass: false }); const assertFn = () => { request._assertSnapshot(response, assertion); }; - assert.throws(assertFn, {message: 'Unable to match snapshot on undefined field undefined foo'}); + assert.throws(assertFn, { + message: "Unable to match snapshot on undefined field undefined foo", + }); }); - it('expect calls _addAssertion [public interface]', function () { - const fn = () => { }; + it("expect calls _addAssertion [public interface]", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new ExpectRequest(fn, jar, opts); - const addSpy = sinon.stub(request, '_addAssertion'); + const addSpy = sinon.stub(request, "_addAssertion"); request.expect(fn); sinon.assert.calledOnce(addSpy); sinon.assert.calledOnceWithMatch(addSpy, { fn: sinon.match.func, // this is the wrapped callback - type: 'body' + type: "body", }); }); - it('expect not ok when given something other than a function [public interface]', function () { - const fn = () => { }; + it("expect not ok when given something other than a function [public interface]", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new ExpectRequest(fn, jar, opts); - const addSpy = sinon.stub(request, '_addAssertion'); + const addSpy = sinon.stub(request, "_addAssertion"); const assertFn = () => { - request.expect('foo'); + request.expect("foo"); }; - assert.throws(assertFn, {message: 'express-test expect() requires a callback function, did you mean expectStatus or expectHeader?'}); + assert.throws(assertFn, { + message: + "express-test expect() requires a callback function, did you mean expectStatus or expectHeader?", + }); sinon.assert.notCalled(addSpy); }); - it('expectStatus calls _addAssertion [public interface]', function () { - const fn = () => { }; + it("expectStatus calls _addAssertion [public interface]", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new ExpectRequest(fn, jar, opts); - const addSpy = sinon.stub(request, '_addAssertion'); + const addSpy = sinon.stub(request, "_addAssertion"); request.expectStatus(200); @@ -557,50 +579,55 @@ describe('ExpectRequest', function () { sinon.assert.calledOnceWithExactly(addSpy, { fn: request._assertStatus, expected: 200, - type: 'status' + type: "status", }); }); - it('expectHeader calls _addAssertion [public interface]', function () { - const fn = () => { }; + it("expectHeader calls _addAssertion [public interface]", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new ExpectRequest(fn, jar, opts); - const addSpy = sinon.stub(request, '_addAssertion'); + const addSpy = sinon.stub(request, "_addAssertion"); - request.expectHeader('foo', 'bar'); + request.expectHeader("foo", "bar"); sinon.assert.calledOnce(addSpy); sinon.assert.calledOnceWithExactly(addSpy, { fn: request._assertHeader, - expectedField: 'foo', - expectedValue: 'bar', - type: 'header' + expectedField: "foo", + expectedValue: "bar", + type: "header", }); }); - it('matchBodySnapshot calls _addAssertion [public interface]', function () { - const fn = () => { }; + it("matchBodySnapshot calls _addAssertion [public interface]", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new ExpectRequest(fn, jar, opts); - const addSpy = sinon.stub(request, '_addAssertion'); + const addSpy = sinon.stub(request, "_addAssertion"); request.matchBodySnapshot({}); sinon.assert.calledOnce(addSpy); - sinon.assert.calledOnceWithExactly(addSpy, {fn: request._assertSnapshot, properties: {}, field: 'body', type: 'body'}); + sinon.assert.calledOnceWithExactly(addSpy, { + fn: request._assertSnapshot, + properties: {}, + field: "body", + type: "body", + }); }); - it('matchHeaderSnapshot calls _addAssertion [public interface]', function () { - const fn = () => { }; + it("matchHeaderSnapshot calls _addAssertion [public interface]", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new ExpectRequest(fn, jar, opts); - const addSpy = sinon.stub(request, '_addAssertion'); + const addSpy = sinon.stub(request, "_addAssertion"); request.matchHeaderSnapshot({}); @@ -608,8 +635,8 @@ describe('ExpectRequest', function () { sinon.assert.calledOnceWithExactly(addSpy, { fn: request._assertSnapshot, properties: {}, - field: 'headers', - type: 'header' + field: "headers", + type: "header", }); }); }); diff --git a/packages/express-test/test/Request.test.js b/packages/express-test/test/Request.test.js index 908d3ed52..8d65d4351 100644 --- a/packages/express-test/test/Request.test.js +++ b/packages/express-test/test/Request.test.js @@ -1,7 +1,7 @@ -const {assert, sinon, stubCookies} = require('./utils'); -const {Request, RequestOptions} = require('../lib/Request'); -const FormData = require('form-data'); -const Stream = require('stream'); +const { assert, sinon, stubCookies } = require("./utils"); +const { Request, RequestOptions } = require("../lib/Request"); +const FormData = require("form-data"); +const Stream = require("stream"); const streamToBuffer = async function (stream) { return new Promise((resolve, reject) => { @@ -12,14 +12,14 @@ const streamToBuffer = async function (stream) { stream.pause(); // just to test the pause method - stream.on('data', onData); + stream.on("data", onData); - stream.on('end', () => { - stream.removeListener('data', onData); // to test the removeListener method + stream.on("end", () => { + stream.removeListener("data", onData); // to test the removeListener method resolve(Buffer.concat(data)); }); - stream.on('error', (err) => { + stream.on("error", (err) => { reject(err); }); @@ -64,14 +64,14 @@ const createAwaitableStream = function () { return ws; }; -describe('Request', function () { +describe("Request", function () { afterEach(function () { sinon.restore(); }); - describe('Class functions', function () { - it('constructor sets app, jar and reqOptions when reqOptions is empty', function () { - const fn = () => { }; + describe("Class functions", function () { + it("constructor sets app, jar and reqOptions when reqOptions is empty", function () { + const fn = () => {}; const jar = {}; const opts = {}; const request = new Request(fn, jar, opts); @@ -80,13 +80,13 @@ describe('Request', function () { assert.equal(request.cookieJar, jar); assert.notEqual(request.reqOptions, opts); assert.equal(request.reqOptions instanceof RequestOptions, true); - assert.equal(request.reqOptions.method, 'GET'); - assert.equal(request.reqOptions.url, '/'); + assert.equal(request.reqOptions.method, "GET"); + assert.equal(request.reqOptions.url, "/"); assert.deepEqual(request.reqOptions.headers, {}); }); - it('constructor sets app, jar and reqOptions', function () { - const fn = () => { }; + it("constructor sets app, jar and reqOptions", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new Request(fn, jar, opts); @@ -96,118 +96,112 @@ describe('Request', function () { assert.equal(request.reqOptions, opts); }); - it('_getReqRes generates req and res correctly', function () { - const fn = () => { }; + it("_getReqRes generates req and res correctly", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new Request(fn, jar, opts); - const {req, res} = request._getReqRes(); + const { req, res } = request._getReqRes(); assert.deepEqual(req.app, fn); assert.deepEqual(res.app, fn); assert.deepEqual(res.req, req); }); - it('_buildResponse handles string buffer as body', function () { - const fn = () => { }; + it("_buildResponse handles string buffer as body", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new Request(fn, jar, opts); - const response = request._buildResponse( - { - statusCode: 999, - body: Buffer.from('Hello World'), - getHeaders: () => { }, - getHeader: () => { - return 'text/html'; - } - - } - ); + const response = request._buildResponse({ + statusCode: 999, + body: Buffer.from("Hello World"), + getHeaders: () => {}, + getHeader: () => { + return "text/html"; + }, + }); assert.equal(response.statusCode, 999); - assert.equal(response.text, 'Hello World'); + assert.equal(response.text, "Hello World"); }); - it('_buildResponse handles JSON buffer as body', function () { - const fn = () => { }; + it("_buildResponse handles JSON buffer as body", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new Request(fn, jar, opts); - const response = request._buildResponse( - { - statusCode: 111, - body: Buffer.from('{"hello":"world"}'), - getHeaders: () => { }, - getHeader: () => { - return 'application/json'; - } - - } - ); + const response = request._buildResponse({ + statusCode: 111, + body: Buffer.from('{"hello":"world"}'), + getHeaders: () => {}, + getHeader: () => { + return "application/json"; + }, + }); assert.equal(response.statusCode, 111); assert.equal(response.text, '{"hello":"world"}'); }); - it('_getCookies', function () { - const fn = () => { }; + it("_getCookies", function () { + const fn = () => {}; const getCookies = { - toValueString: sinon.stub() + toValueString: sinon.stub(), }; const jar = { - getCookies: sinon.stub().returns(getCookies) + getCookies: sinon.stub().returns(getCookies), }; const opts = new RequestOptions(); const request = new Request(fn, jar, opts); - const req = {url: '/'}; + const req = { url: "/" }; request._getCookies(req); sinon.assert.calledOnce(jar.getCookies); - sinon.assert.calledOnceWithMatch(jar.getCookies, sinon.match({path: '/'})); + sinon.assert.calledOnceWithMatch(jar.getCookies, sinon.match({ path: "/" })); sinon.assert.calledOnce(getCookies.toValueString); }); - it('_restoreCookies does nothing with no cookies', function () { - const fn = () => { }; + it("_restoreCookies does nothing with no cookies", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new Request(fn, jar, opts); request._getCookies = sinon.stub(); - const req = {headers: {}}; + const req = { headers: {} }; request._restoreCookies(req); assert.equal(req.headers.cookie, undefined); }); - it('_restoreCookies restores cookes when present', function () { - const fn = () => { }; + it("_restoreCookies restores cookes when present", function () { + const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new Request(fn, jar, opts); - request._getCookies = sinon.stub().returns('abc'); + request._getCookies = sinon.stub().returns("abc"); - const req = {headers: {}}; + const req = { headers: {} }; request._restoreCookies(req); - assert.equal(req.headers.cookie, 'abc'); + assert.equal(req.headers.cookie, "abc"); }); - it('_saveCookies does nothing with no cookies', function () { - const fn = () => { }; + it("_saveCookies does nothing with no cookies", function () { + const fn = () => {}; const jar = { - setCookies: sinon.stub() + setCookies: sinon.stub(), }; const opts = new RequestOptions(); const request = new Request(fn, jar, opts); const res = { - getHeader: sinon.stub() + getHeader: sinon.stub(), }; request._saveCookies(res); @@ -216,37 +210,37 @@ describe('Request', function () { sinon.assert.notCalled(jar.setCookies); }); - it('_saveCookies sets cookies on the cookiejar', function () { - const fn = () => { }; + it("_saveCookies sets cookies on the cookiejar", function () { + const fn = () => {}; const jar = { - setCookies: sinon.stub() + setCookies: sinon.stub(), }; const opts = new RequestOptions(); const request = new Request(fn, jar, opts); const res = { - getHeader: sinon.stub().returns('xyz') + getHeader: sinon.stub().returns("xyz"), }; request._saveCookies(res); sinon.assert.calledOnce(res.getHeader); sinon.assert.calledOnce(jar.setCookies); - sinon.assert.calledOnceWithMatch(jar.setCookies, 'xyz'); + sinon.assert.calledOnceWithMatch(jar.setCookies, "xyz"); }); - it('_doRequest', async function () { + it("_doRequest", async function () { await new Promise((resolve, reject) => { const fn = (req, res) => { // This is how reqresnext works - res.emit('finish'); + res.emit("finish"); }; const jar = {}; const opts = new RequestOptions(); const request = new Request(fn, jar, opts); // Stub cookies, we'll test this behaviour later - const {saveCookiesStub, restoreCookiesStub} = stubCookies(request); + const { saveCookiesStub, restoreCookiesStub } = stubCookies(request); request._doRequest((error, response) => { if (error) { @@ -264,97 +258,100 @@ describe('Request', function () { }); }); - it('body() sets body correctly', function () { + it("body() sets body correctly", function () { const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new Request(fn, jar, opts); - const body = {foo: 'bar'}; + const body = { foo: "bar" }; request.body(body); assert.equal(request.reqOptions.body, body); }); - it('body() sets body correctly with FormData', async function () { + it("body() sets body correctly with FormData", async function () { const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new Request(fn, jar, opts); const formData = new FormData(); - formData.append('foo', 'bar'); + formData.append("foo", "bar"); request.body(formData); assert.equal(request.reqOptions.body, undefined); - const {req} = request._getReqRes(); - const requestBody = (await streamToBuffer(req)).toString('utf8'); + const { req } = request._getReqRes(); + const requestBody = (await streamToBuffer(req)).toString("utf8"); assert.match(requestBody, /Content-Disposition: form-data; name="foo"\r\n\r\nbar/); - assert.equal(req.headers['content-length'], requestBody.length); - assert.equal(req.headers['content-type'], 'multipart/form-data; boundary=' + formData.getBoundary()); + assert.equal(req.headers["content-length"], requestBody.length); + assert.equal( + req.headers["content-type"], + "multipart/form-data; boundary=" + formData.getBoundary(), + ); }); - it('body() sets body correctly with string', async function () { + it("body() sets body correctly with string", async function () { const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new Request(fn, jar, opts); - const stringData = 'hello-world'; + const stringData = "hello-world"; request.body(stringData); assert.equal(request.reqOptions.body, undefined); - const {req} = request._getReqRes(); - const requestBody = (await streamToBuffer(req)).toString('utf8'); + const { req } = request._getReqRes(); + const requestBody = (await streamToBuffer(req)).toString("utf8"); assert.equal(requestBody, stringData); - assert.equal(req.headers['content-length'], stringData.length); + assert.equal(req.headers["content-length"], stringData.length); }); - it('body() stream can pipe to writeable stream', async function () { + it("body() stream can pipe to writeable stream", async function () { const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new Request(fn, jar, opts); - const stringData = 'hello-world'; + const stringData = "hello-world"; request.body(stringData); assert.equal(request.reqOptions.body, undefined); - const {req} = request._getReqRes(); + const { req } = request._getReqRes(); const writeableStream = createAwaitableStream(); req.pipe(writeableStream); - const requestBody = (await writeableStream).toString('utf8'); + const requestBody = (await writeableStream).toString("utf8"); assert.equal(requestBody, stringData); - assert.equal(req.headers['content-length'], stringData.length); + assert.equal(req.headers["content-length"], stringData.length); req.unpipe(writeableStream); }); - it('header() sets body correctly', function () { + it("header() sets body correctly", function () { const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new Request(fn, jar, opts); - request.header('foo', 'bar'); + request.header("foo", "bar"); - assert.equal(request.reqOptions.headers.foo, 'bar'); + assert.equal(request.reqOptions.headers.foo, "bar"); }); - it('attach() sets body correctly with single file', async function () { + it("attach() sets body correctly with single file", async function () { const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new Request(fn, jar, opts); - const path = require('path'); - const filePath = path.join(__dirname, 'fixtures/ghost-favicon.png'); + const path = require("path"); + const filePath = path.join(__dirname, "fixtures/ghost-favicon.png"); - request.attach('image', filePath); + request.attach("image", filePath); assert.equal(request._formData instanceof FormData, true); assert.equal(request.reqOptions.body, undefined); @@ -366,24 +363,22 @@ describe('Request', function () { assert.match(content, /filename="ghost-favicon.png"/); }); - it('attach() sets body correctly with multiple files', async function () { + it("attach() sets body correctly with multiple files", async function () { const fn = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new Request(fn, jar, opts); - const path = require('path'); - const fs = require('fs'); - const imagePath = path.join(__dirname, 'fixtures/ghost-favicon.png'); - const textPath = path.join(__dirname, 'fixtures/test-multi.txt'); + const path = require("path"); + const fs = require("fs"); + const imagePath = path.join(__dirname, "fixtures/ghost-favicon.png"); + const textPath = path.join(__dirname, "fixtures/test-multi.txt"); // Create a test text file - fs.writeFileSync(textPath, 'test content'); + fs.writeFileSync(textPath, "test content"); try { - request - .attach('image', imagePath) - .attach('document', textPath); + request.attach("image", imagePath).attach("document", textPath); assert.equal(request._formData instanceof FormData, true); @@ -400,10 +395,10 @@ describe('Request', function () { } }); - it('class is thenable [public api]', async function () { + it("class is thenable [public api]", async function () { const fn = (req, res) => { // This is how reqresnext works - res.emit('finish'); + res.emit("finish"); }; const jar = {}; const opts = new RequestOptions(); @@ -416,13 +411,15 @@ describe('Request', function () { const response = await request; assert.equal(response.statusCode, 200); // this is the default } catch (error) { - assert.fail(`This should not have thrown an error. Original error: ${error.message}.`); + assert.fail( + `This should not have thrown an error. Original error: ${error.message}.`, + ); } }); - it('express errors are handled correctly', async function () { + it("express errors are handled correctly", async function () { const fn = () => { - throw new Error('something went wrong'); + throw new Error("something went wrong"); }; const jar = {}; const opts = new RequestOptions(); @@ -433,22 +430,22 @@ describe('Request', function () { try { await request; - assert.fail('Should have errored'); + assert.fail("Should have errored"); } catch (error) { - assert.equal(error.message, 'something went wrong'); + assert.equal(error.message, "something went wrong"); } }); }); - describe('Testing with Express Internals', function () { - it('converts body to text correctly for string', async function () { + describe("Testing with Express Internals", function () { + it("converts body to text correctly for string", async function () { const fn = (req, res) => { // This is how express works - res.send('Hello World!'); - res.emit('finish'); + res.send("Hello World!"); + res.emit("finish"); }; // Used by express internally to get etag function in .send() - fn.get = () => { }; + fn.get = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new Request(fn, jar, opts); @@ -460,20 +457,22 @@ describe('Request', function () { try { response = await request; } catch (error) { - assert.fail(`This should not have thrown an error. Original error: ${error.stack}.`); + assert.fail( + `This should not have thrown an error. Original error: ${error.stack}.`, + ); } assert.equal(response.statusCode, 200); // this is the default - assert.equal(response.text, 'Hello World!'); + assert.equal(response.text, "Hello World!"); }); - it('converts body to text correctly for json', async function () { + it("converts body to text correctly for json", async function () { const fn = (req, res) => { - res.json({hello: 'world'}); - res.emit('finish'); + res.json({ hello: "world" }); + res.emit("finish"); }; // Used by express internally to get etag function in .send() - fn.get = () => { }; + fn.get = () => {}; const jar = {}; const opts = new RequestOptions(); const request = new Request(fn, jar, opts); @@ -484,12 +483,14 @@ describe('Request', function () { try { response = await request; } catch (error) { - assert.fail(`This should not have thrown an error. Original error: ${error.stack}.`); + assert.fail( + `This should not have thrown an error. Original error: ${error.stack}.`, + ); } assert.equal(response.statusCode, 200); // this is the default assert.equal(response.text, '{"hello":"world"}'); - assert.deepEqual(response.body, {hello: 'world'}); + assert.deepEqual(response.body, { hello: "world" }); }); }); }); diff --git a/packages/express-test/test/example-app.test.js b/packages/express-test/test/example-app.test.js index 140b124d3..800f3820a 100644 --- a/packages/express-test/test/example-app.test.js +++ b/packages/express-test/test/example-app.test.js @@ -1,11 +1,11 @@ -const {assert} = require('./utils'); -const path = require('path'); - -const Agent = require('../'); // we require the root file -const app = require('../example/app'); -const {any} = require('@tryghost/jest-snapshot'); -const {promises: fs, createReadStream} = require('node:fs'); -const FormData = require('form-data'); +const { assert } = require("./utils"); +const path = require("path"); + +const Agent = require("../"); // we require the root file +const app = require("../example/app"); +const { any } = require("@tryghost/jest-snapshot"); +const { promises: fs, createReadStream } = require("node:fs"); +const FormData = require("form-data"); let agent; /** @@ -21,42 +21,42 @@ async function getAgent() { } async function getAPIAgent() { - return new Agent(app, {baseUrl: '/api/'}); + return new Agent(app, { baseUrl: "/api/" }); } async function getExtendedAPIAgent() { agent = await getAPIAgent(); agent.login = async function () { - return await agent.post('/session/', { + return await agent.post("/session/", { body: { - username: 'hello', - password: 'world' - } + username: "hello", + password: "world", + }, }); }; return agent; } -describe('Example App', function () { +describe("Example App", function () { beforeAll(async function () { agent = await getAgent(); }); - it('GET / works', async function () { + it("GET / works", async function () { try { - const {statusCode, text} = await agent.get('/'); + const { statusCode, text } = await agent.get("/"); assert.equal(statusCode, 200); - assert.equal(text, 'Hello World!'); + assert.equal(text, "Hello World!"); } catch (error) { assert.fail(`Should not have thrown an error', but got ${error.stack}`); } }); - it('GET /idontexist 404s', async function () { + it("GET /idontexist 404s", async function () { try { - const {statusCode, text} = await agent.get('/idontexist'); + const { statusCode, text } = await agent.get("/idontexist"); assert.equal(statusCode, 404); assert.match(text, /Cannot GET \/idontexist/); } catch (error) { @@ -64,339 +64,382 @@ describe('Example App', function () { } }); - describe('API Agent with authentication in two steps', function () { + describe("API Agent with authentication in two steps", function () { beforeAll(async function () { agent = await getAPIAgent(); }); - it('cannot perform request without session', async function () { - const {statusCode, headers, body, text} = await agent.get('/foo/'); + it("cannot perform request without session", async function () { + const { statusCode, headers, body, text } = await agent.get("/foo/"); assert.equal(statusCode, 403); - assert.deepEqual(Object.keys(headers), ['x-powered-by', 'content-type', 'content-length', 'etag']); + assert.deepEqual(Object.keys(headers), [ + "x-powered-by", + "content-type", + "content-length", + "etag", + ]); assert.deepEqual(body, {}); - assert.equal(text, 'Forbidden'); + assert.equal(text, "Forbidden"); }); - it('cannot create a session without valid credentials', async function () { - const sessionRes = await agent.post('/session/', { + it("cannot create a session without valid credentials", async function () { + const sessionRes = await agent.post("/session/", { body: { - username: 'hello' - } + username: "hello", + }, }); assert.equal(sessionRes.statusCode, 401); - assert.deepEqual(Object.keys(sessionRes.headers), ['x-powered-by', 'content-type', 'content-length', 'etag']); + assert.deepEqual(Object.keys(sessionRes.headers), [ + "x-powered-by", + "content-type", + "content-length", + "etag", + ]); assert.deepEqual(sessionRes.body, {}); - assert.equal(sessionRes.text, 'Unauthorized'); + assert.equal(sessionRes.text, "Unauthorized"); }); - it('create session & make authenticated request', async function () { - const sessionRes = await agent.post('/session/', { + it("create session & make authenticated request", async function () { + const sessionRes = await agent.post("/session/", { body: { - username: 'hello', - password: 'world' - } + username: "hello", + password: "world", + }, }); assert.equal(sessionRes.statusCode, 200); - assert.deepEqual(Object.keys(sessionRes.headers), ['x-powered-by', 'content-type', 'content-length', 'etag', 'set-cookie']); + assert.deepEqual(Object.keys(sessionRes.headers), [ + "x-powered-by", + "content-type", + "content-length", + "etag", + "set-cookie", + ]); assert.deepEqual(sessionRes.body, {}); - assert.equal(sessionRes.text, 'OK'); + assert.equal(sessionRes.text, "OK"); - const {statusCode, headers, body, text} = await agent.get('/foo/'); + const { statusCode, headers, body, text } = await agent.get("/foo/"); assert.equal(statusCode, 200); - assert.deepEqual(Object.keys(headers), ['x-powered-by', 'content-type', 'content-length', 'etag']); - assert.deepEqual(body, {foo: [{bar: 'baz'}]}); + assert.deepEqual(Object.keys(headers), [ + "x-powered-by", + "content-type", + "content-length", + "etag", + ]); + assert.deepEqual(body, { foo: [{ bar: "baz" }] }); assert.equal(text, '{"foo":[{"bar":"baz"}]}'); }); }); - describe('API Agent with login function', function () { + describe("API Agent with login function", function () { beforeAll(async function () { agent = await getExtendedAPIAgent(); await agent.login(); }); - it('make an authenticated request', async function () { - const {statusCode, headers, body, text} = await agent.get('/foo/'); + it("make an authenticated request", async function () { + const { statusCode, headers, body, text } = await agent.get("/foo/"); assert.equal(statusCode, 200); - assert.deepEqual(Object.keys(headers), ['x-powered-by', 'content-type', 'content-length', 'etag']); - assert.deepEqual(body, {foo: [{bar: 'baz'}]}); + assert.deepEqual(Object.keys(headers), [ + "x-powered-by", + "content-type", + "content-length", + "etag", + ]); + assert.deepEqual(body, { foo: [{ bar: "baz" }] }); assert.equal(text, '{"foo":[{"bar":"baz"}]}'); }); }); - describe('Cookie clearing functionality', function () { + describe("Cookie clearing functionality", function () { beforeAll(async function () { agent = await getAPIAgent(); }); - it('clearCookies removes session and requires re-authentication', async function () { + it("clearCookies removes session and requires re-authentication", async function () { // First, create a session - const sessionRes = await agent.post('/session/', { + const sessionRes = await agent.post("/session/", { body: { - username: 'hello', - password: 'world' - } + username: "hello", + password: "world", + }, }); assert.equal(sessionRes.statusCode, 200); // Verify we can make authenticated requests - const authRes = await agent.get('/foo/'); + const authRes = await agent.get("/foo/"); assert.equal(authRes.statusCode, 200); - assert.deepEqual(authRes.body, {foo: [{bar: 'baz'}]}); + assert.deepEqual(authRes.body, { foo: [{ bar: "baz" }] }); // Clear cookies agent.clearCookies(); // Verify we can no longer make authenticated requests - const unauthRes = await agent.get('/foo/'); + const unauthRes = await agent.get("/foo/"); assert.equal(unauthRes.statusCode, 403); - assert.equal(unauthRes.text, 'Forbidden'); + assert.equal(unauthRes.text, "Forbidden"); }); - it('clearCookies supports chaining', async function () { + it("clearCookies supports chaining", async function () { // Create a session - await agent.post('/session/', { + await agent.post("/session/", { body: { - username: 'hello', - password: 'world' - } + username: "hello", + password: "world", + }, }); // Verify authenticated request works - const authRes = await agent.get('/foo/'); + const authRes = await agent.get("/foo/"); assert.equal(authRes.statusCode, 200); // Use logout and chain a request - const unauthRes = await agent.clearCookies().get('/foo/'); + const unauthRes = await agent.clearCookies().get("/foo/"); assert.equal(unauthRes.statusCode, 403); - assert.equal(unauthRes.text, 'Forbidden'); + assert.equal(unauthRes.text, "Forbidden"); }); }); - describe('Set & Expect', function () { + describe("Set & Expect", function () { beforeAll(async function () { agent = await getAgent(); }); - it('set headers but not body using reqOptions', async function () { - const {statusCode, headers, body} = await agent - .post('/check/', { - headers: {'x-check': true} - }); + it("set headers but not body using reqOptions", async function () { + const { statusCode, headers, body } = await agent.post("/check/", { + headers: { "x-check": true }, + }); assert.equal(statusCode, 200); assert.deepEqual(body, {}); - assert.equal(headers['x-checked'], 'true'); + assert.equal(headers["x-checked"], "true"); }); - it('set headers, status and body using reqOptions', async function () { - const {statusCode, headers, body} = await agent - .post('/check/', { - body: {foo: 'bar'}, - headers: {'x-check': true} - }); + it("set headers, status and body using reqOptions", async function () { + const { statusCode, headers, body } = await agent.post("/check/", { + body: { foo: "bar" }, + headers: { "x-check": true }, + }); assert.equal(statusCode, 200); - assert.deepEqual(body, {foo: 'bar'}); - assert.equal(headers['x-checked'], 'true'); + assert.deepEqual(body, { foo: "bar" }); + assert.equal(headers["x-checked"], "true"); }); - it('set headers, status and body using set chaining', async function () { - const {statusCode, headers, body} = await agent - .post('/check/') - .body({foo: 'bar'}) - .header('x-check', true); + it("set headers, status and body using set chaining", async function () { + const { statusCode, headers, body } = await agent + .post("/check/") + .body({ foo: "bar" }) + .header("x-check", true); assert.equal(statusCode, 200); - assert.deepEqual(body, {foo: 'bar'}); - assert.equal(headers['x-checked'], 'true'); + assert.deepEqual(body, { foo: "bar" }); + assert.equal(headers["x-checked"], "true"); }); - it('set headers, status and body with mixed-case header', async function () { - const {statusCode, headers, body} = await agent - .post('/check/', { - body: {foo: 'bar'}, - headers: {'X-Check': true} - }); + it("set headers, status and body with mixed-case header", async function () { + const { statusCode, headers, body } = await agent.post("/check/", { + body: { foo: "bar" }, + headers: { "X-Check": true }, + }); assert.equal(statusCode, 200); - assert.deepEqual(body, {foo: 'bar'}); - assert.equal(headers['x-checked'], 'true'); + assert.deepEqual(body, { foo: "bar" }); + assert.equal(headers["x-checked"], "true"); }); - it('set headers, status and body with mixed-case header and chaining', async function () { - const {statusCode, headers, body} = await agent - .post('/check/') - .body({foo: 'bar'}) - .header('X-Check', true); + it("set headers, status and body with mixed-case header and chaining", async function () { + const { statusCode, headers, body } = await agent + .post("/check/") + .body({ foo: "bar" }) + .header("X-Check", true); assert.equal(statusCode, 200); - assert.deepEqual(body, {foo: 'bar'}); - assert.equal(headers['x-checked'], 'true'); + assert.deepEqual(body, { foo: "bar" }); + assert.equal(headers["x-checked"], "true"); }); - it('check headers, status and body using set and expect chaining', async function () { + it("check headers, status and body using set and expect chaining", async function () { await agent - .post('/check/') - .body({foo: 'bar'}) - .header('x-check', true) + .post("/check/") + .body({ foo: "bar" }) + .header("x-check", true) .expectStatus(200) - .expectHeader('x-checked', 'true') - .expect(({body}) => { - assert.deepEqual(body, {foo: 'bar'}); + .expectHeader("x-checked", "true") + .expect(({ body }) => { + assert.deepEqual(body, { foo: "bar" }); }); }); - it('check headers, status and body using set, expect chaining & snapshot matching', async function () { + it("check headers, status and body using set, expect chaining & snapshot matching", async function () { await agent - .post('/check/') - .body({foo: 'bar'}) - .header('x-check', true) + .post("/check/") + .body({ foo: "bar" }) + .header("x-check", true) .expectStatus(200) - .expectHeader('x-checked', 'true') + .expectHeader("x-checked", "true") .matchBodySnapshot() .matchHeaderSnapshot(); }); - it('check status using expect chaining errors correctly', async function () { - await assert.rejects(async () => { - return await agent - .post('/check/') - .expectStatus(404); - }), {message: 'Expected header "x-checked: false", got "x-checked: true" POST request on /check/'}; + it("check status using expect chaining errors correctly", async function () { + (await assert.rejects(async () => { + return await agent.post("/check/").expectStatus(404); + }), + { + message: + 'Expected header "x-checked: false", got "x-checked: true" POST request on /check/', + }); }); - it('check headers, status and empty body using set and expect chaining', async function () { + it("check headers, status and empty body using set and expect chaining", async function () { await agent - .post('/check/') - .header('x-check', true) + .post("/check/") + .header("x-check", true) .expectStatus(200) - .expectHeader('x-checked', 'true') + .expectHeader("x-checked", "true") .expectEmptyBody(); }); - it('check header using expect chaining errors correctly', async function () { - await assert.rejects(async () => { - return await agent - .post('/check/') - .header('x-check', true) - .expectStatus(200) - .expectHeader('x-checked', 'false'); - }, {message: 'Expected header "x-checked: false", got "x-checked: true" POST request on /check/'}); + it("check header using expect chaining errors correctly", async function () { + await assert.rejects( + async () => { + return await agent + .post("/check/") + .header("x-check", true) + .expectStatus(200) + .expectHeader("x-checked", "false"); + }, + { + message: + 'Expected header "x-checked: false", got "x-checked: true" POST request on /check/', + }, + ); }); - it('check body using expect chaining errors correctly', async function () { - await assert.rejects(async () => { - return await agent - .post('/check/') - .body({foo: 'bar'}) - .expect(({body}) => { - assert.deepEqual(body, {foo: 'ba'}); - }); - }, (error) => { - assert.match(error.message, /^Expected values to be loosely deep-equal/); - return true; - }); + it("check body using expect chaining errors correctly", async function () { + await assert.rejects( + async () => { + return await agent + .post("/check/") + .body({ foo: "bar" }) + .expect(({ body }) => { + assert.deepEqual(body, { foo: "ba" }); + }); + }, + (error) => { + assert.match(error.message, /^Expected values to be loosely deep-equal/); + return true; + }, + ); }); - it('check empty body using expect chaining errors correctly', async function () { - await assert.rejects(async () => { - return await agent - .post('/check/') - .body({foo: 'bar'}) - .expectEmptyBody(); - }, (error) => { - assert.match(error.message, /^Expected body to be empty, got/); - return true; - }); + it("check empty body using expect chaining errors correctly", async function () { + await assert.rejects( + async () => { + return await agent.post("/check/").body({ foo: "bar" }).expectEmptyBody(); + }, + (error) => { + assert.match(error.message, /^Expected body to be empty, got/); + return true; + }, + ); }); - it('check body using snapshot matching errors correctly for missing property', async function () { - await assert.rejects(async () => { - return await agent - .post('/check/') - .body({ - foo: 'bar' - }) - .matchBodySnapshot({ - id: any(String) - }); - }, (error) => { - assert.match(error.message, /check body using snapshot matching errors correctly for missing property/); - assert.match(error.message, /\[body\]/); - assert.match(error.message, /Expected properties {2}- 1/); - assert.match(error.message, /Received value {2,}\+ 1/); - return true; - }); + it("check body using snapshot matching errors correctly for missing property", async function () { + await assert.rejects( + async () => { + return await agent + .post("/check/") + .body({ + foo: "bar", + }) + .matchBodySnapshot({ + id: any(String), + }); + }, + (error) => { + assert.match( + error.message, + /check body using snapshot matching errors correctly for missing property/, + ); + assert.match(error.message, /\[body\]/); + assert.match(error.message, /Expected properties {2}- 1/); + assert.match(error.message, /Received value {2,}\+ 1/); + return true; + }, + ); }); - it('check body using snapshot matching errors correctly for random data', async function () { - await assert.rejects(async () => { - return await agent - .post('/check/') - .body({ - foo: 'bar', - id: Math.random().toString(36) - }) - .matchBodySnapshot(); - }, (error) => { - assert.match(error.message, /check body using snapshot matching errors correctly for random data/); - assert.match(error.message, /\[body\]/); - assert.match(error.message, /Snapshot {2}- 1/); - assert.match(error.message, /Received {2}\+ 1/); - return true; - }); + it("check body using snapshot matching errors correctly for random data", async function () { + await assert.rejects( + async () => { + return await agent + .post("/check/") + .body({ + foo: "bar", + id: Math.random().toString(36), + }) + .matchBodySnapshot(); + }, + (error) => { + assert.match( + error.message, + /check body using snapshot matching errors correctly for random data/, + ); + assert.match(error.message, /\[body\]/); + assert.match(error.message, /Snapshot {2}- 1/); + assert.match(error.message, /Received {2}\+ 1/); + return true; + }, + ); }); - it('check body using snapshot matching properties works for random data', async function () { + it("check body using snapshot matching properties works for random data", async function () { await agent - .post('/check/') + .post("/check/") .body({ - foo: 'bar', - id: Math.random().toString(36) + foo: "bar", + id: Math.random().toString(36), }) .matchBodySnapshot({ - id: any(String) + id: any(String), }); }); - it('can send the body as a string', async function () { - const data = {foo: 'bar', nested: {foo: 'bar'}}; + it("can send the body as a string", async function () { + const data = { foo: "bar", nested: { foo: "bar" } }; await agent - .post('/api/ping/') + .post("/api/ping/") .body(JSON.stringify(data)) - .header('content-type', 'application/json') + .header("content-type", "application/json") .expectStatus(200) - .expect(({body}) => { + .expect(({ body }) => { assert.deepEqual(body, data); }); }); - it('can upload a file', async function () { - const fileContents = await fs.readFile(__dirname + '/fixtures/ghost-favicon.png'); - const filename = 'test.png'; - const contentType = 'image/png'; + it("can upload a file", async function () { + const fileContents = await fs.readFile(__dirname + "/fixtures/ghost-favicon.png"); + const filename = "test.png"; + const contentType = "image/png"; const form = new FormData(); - form.append('image', fileContents, { + form.append("image", fileContents, { filename, - contentType + contentType, }); - const {body} = await agent - .post('/api/upload/') - .body(form) - .expectStatus(200); + const { body } = await agent.post("/api/upload/").body(form).expectStatus(200); assert.equal(body.originalname, filename); assert.equal(body.mimetype, contentType); assert.equal(body.size, fileContents.length); - assert.equal(body.fieldname, 'image'); + assert.equal(body.fieldname, "image"); // Do a real comparison in the uploaded file to check if it was uploaded and saved correctly const uploadedFileContents = await fs.readFile(body.path); @@ -410,18 +453,18 @@ describe('Example App', function () { } }); - it('can upload a file using the attach method', async function () { - const fileContents = await fs.readFile(__dirname + '/fixtures/ghost-favicon.png'); + it("can upload a file using the attach method", async function () { + const fileContents = await fs.readFile(__dirname + "/fixtures/ghost-favicon.png"); - const {body} = await agent - .post('/api/upload/') - .attach('image', path.join(__dirname, '/fixtures/ghost-favicon.png')) + const { body } = await agent + .post("/api/upload/") + .attach("image", path.join(__dirname, "/fixtures/ghost-favicon.png")) .expectStatus(200); - assert.equal(body.originalname, 'ghost-favicon.png'); - assert.equal(body.mimetype, 'image/png'); + assert.equal(body.originalname, "ghost-favicon.png"); + assert.equal(body.mimetype, "image/png"); assert.equal(body.size, fileContents.length); - assert.equal(body.fieldname, 'image'); + assert.equal(body.fieldname, "image"); // Do a real comparison in the uploaded file to check if it was uploaded and saved correctly const uploadedFileContents = await fs.readFile(body.path); @@ -435,30 +478,30 @@ describe('Example App', function () { } }); - it('can upload multiple files using multiple attach calls', async function () { + it("can upload multiple files using multiple attach calls", async function () { // Create a text file for testing - const textFilePath = path.join(__dirname, '/fixtures/test-upload.txt'); - await fs.writeFile(textFilePath, 'test content for multiple files'); + const textFilePath = path.join(__dirname, "/fixtures/test-upload.txt"); + await fs.writeFile(textFilePath, "test content for multiple files"); - const imageContents = await fs.readFile(__dirname + '/fixtures/ghost-favicon.png'); + const imageContents = await fs.readFile(__dirname + "/fixtures/ghost-favicon.png"); const textContents = await fs.readFile(textFilePath); - const {body} = await agent - .post('/api/upload-multiple/') - .attach('image', path.join(__dirname, '/fixtures/ghost-favicon.png')) - .attach('document', textFilePath) + const { body } = await agent + .post("/api/upload-multiple/") + .attach("image", path.join(__dirname, "/fixtures/ghost-favicon.png")) + .attach("document", textFilePath) .expectStatus(200); // Verify both files were uploaded - assert.equal(body.image[0].originalname, 'ghost-favicon.png'); - assert.equal(body.image[0].mimetype, 'image/png'); + assert.equal(body.image[0].originalname, "ghost-favicon.png"); + assert.equal(body.image[0].mimetype, "image/png"); assert.equal(body.image[0].size, imageContents.length); - assert.equal(body.image[0].fieldname, 'image'); + assert.equal(body.image[0].fieldname, "image"); - assert.equal(body.document[0].originalname, 'test-upload.txt'); - assert.equal(body.document[0].mimetype, 'text/plain'); + assert.equal(body.document[0].originalname, "test-upload.txt"); + assert.equal(body.document[0].mimetype, "text/plain"); assert.equal(body.document[0].size, textContents.length); - assert.equal(body.document[0].fieldname, 'document'); + assert.equal(body.document[0].fieldname, "document"); // Clean up uploaded files try { @@ -470,19 +513,22 @@ describe('Example App', function () { } }); - it('can stream body', async function () { - const stat = await fs.stat(__dirname + '/fixtures/long-json-body.json'); - const stream = createReadStream(__dirname + '/fixtures/long-json-body.json'); + it("can stream body", async function () { + const stat = await fs.stat(__dirname + "/fixtures/long-json-body.json"); + const stream = createReadStream(__dirname + "/fixtures/long-json-body.json"); - const {body} = await agent - .post('/api/ping/') - .header('content-type', 'application/json') - .header('content-length', stat.size) // the Express json middleware requires content-length to work + const { body } = await agent + .post("/api/ping/") + .header("content-type", "application/json") + .header("content-length", stat.size) // the Express json middleware requires content-length to work .stream(stream) .expectStatus(200); - const fileContents = await fs.readFile(__dirname + '/fixtures/long-json-body.json', 'utf8'); - assert.equal(JSON.stringify(body) + '\n', fileContents); + const fileContents = await fs.readFile( + __dirname + "/fixtures/long-json-body.json", + "utf8", + ); + assert.equal(JSON.stringify(body) + "\n", fileContents); }); }); }); diff --git a/packages/express-test/test/utils.test.js b/packages/express-test/test/utils.test.js index e9410bacf..6bea0f0da 100644 --- a/packages/express-test/test/utils.test.js +++ b/packages/express-test/test/utils.test.js @@ -1,61 +1,61 @@ -const {assert} = require('./utils'); -const path = require('path'); -const FormData = require('form-data'); +const { assert } = require("./utils"); +const path = require("path"); +const FormData = require("form-data"); -const {isJSON, normalizeURL, attachFile} = require('../lib/utils'); +const { isJSON, normalizeURL, attachFile } = require("../lib/utils"); -describe('Utils', function () { - it('isJSON', function () { - assert.equal(isJSON('application/json'), true); - assert.equal(isJSON('application/ld+json'), true); - assert.equal(isJSON('text/html'), false); +describe("Utils", function () { + it("isJSON", function () { + assert.equal(isJSON("application/json"), true); + assert.equal(isJSON("application/ld+json"), true); + assert.equal(isJSON("text/html"), false); }); - describe('normalizeURL', function () { - it('adds trailing slash in the end of URL', function () { - const url = normalizeURL('http://example.com'); - assert.equal(url, 'http://example.com/'); + describe("normalizeURL", function () { + it("adds trailing slash in the end of URL", function () { + const url = normalizeURL("http://example.com"); + assert.equal(url, "http://example.com/"); }); - it('does NOT add trailing slash in the end of URL if it is present', function () { - const url = normalizeURL('http://example.com/'); - assert.equal(url, 'http://example.com/'); + it("does NOT add trailing slash in the end of URL if it is present", function () { + const url = normalizeURL("http://example.com/"); + assert.equal(url, "http://example.com/"); }); - it('adds trailing slash in the end of URL respecting query string', function () { - const url = normalizeURL('http://example.com?yolo=9000'); - assert.equal(url, 'http://example.com/?yolo=9000'); + it("adds trailing slash in the end of URL respecting query string", function () { + const url = normalizeURL("http://example.com?yolo=9000"); + assert.equal(url, "http://example.com/?yolo=9000"); }); - it('does NOT add trailing slash in the end of URL respecting query string', function () { - const url = normalizeURL('http://example.com/?yolo=9000'); - assert.equal(url, 'http://example.com/?yolo=9000'); + it("does NOT add trailing slash in the end of URL respecting query string", function () { + const url = normalizeURL("http://example.com/?yolo=9000"); + assert.equal(url, "http://example.com/?yolo=9000"); }); }); - describe('attachFile', function () { - it('creates FormData with file content', function () { - const filePath = path.join(__dirname, 'fixtures/test-file.txt'); - const formData = attachFile('testfile', filePath); + describe("attachFile", function () { + it("creates FormData with file content", function () { + const filePath = path.join(__dirname, "fixtures/test-file.txt"); + const formData = attachFile("testfile", filePath); assert.equal(formData instanceof FormData, true); - assert.equal(typeof formData.getHeaders, 'function'); - assert.equal(typeof formData.getBuffer, 'function'); + assert.equal(typeof formData.getHeaders, "function"); + assert.equal(typeof formData.getBuffer, "function"); }); - it('sets correct filename from path', function () { - const filePath = path.join(__dirname, 'fixtures/test-file.txt'); - const formData = attachFile('testfile', filePath); + it("sets correct filename from path", function () { + const filePath = path.join(__dirname, "fixtures/test-file.txt"); + const formData = attachFile("testfile", filePath); // FormData doesn't expose a direct way to check the filename, // but we can verify it was created without errors const headers = formData.getHeaders(); - assert.match(headers['content-type'], /^multipart\/form-data; boundary=/); + assert.match(headers["content-type"], /^multipart\/form-data; boundary=/); }); - it('sets correct content type for text file', function () { - const filePath = path.join(__dirname, 'fixtures/test-file.txt'); - const formData = attachFile('testfile', filePath); + it("sets correct content type for text file", function () { + const filePath = path.join(__dirname, "fixtures/test-file.txt"); + const formData = attachFile("testfile", filePath); // Get the form data buffer to check it contains the right content type const buffer = formData.getBuffer(); @@ -65,9 +65,9 @@ describe('Utils', function () { assert.match(content, /Content-Type: text\/plain/); }); - it('sets correct content type for PNG image', function () { - const filePath = path.join(__dirname, 'fixtures/ghost-favicon.png'); - const formData = attachFile('image', filePath); + it("sets correct content type for PNG image", function () { + const filePath = path.join(__dirname, "fixtures/ghost-favicon.png"); + const formData = attachFile("image", filePath); // Get the form data buffer to check it contains the right content type const buffer = formData.getBuffer(); @@ -77,14 +77,14 @@ describe('Utils', function () { assert.match(content, /Content-Type: image\/png/); }); - it('uses default content type for unknown file extension', function () { + it("uses default content type for unknown file extension", function () { // Create a file with unknown extension - const fs = require('fs'); - const unknownFile = path.join(__dirname, 'fixtures/test.unknown'); - fs.writeFileSync(unknownFile, 'test content'); + const fs = require("fs"); + const unknownFile = path.join(__dirname, "fixtures/test.unknown"); + fs.writeFileSync(unknownFile, "test content"); try { - const formData = attachFile('file', unknownFile); + const formData = attachFile("file", unknownFile); const buffer = formData.getBuffer(); const content = buffer.toString(); @@ -96,9 +96,9 @@ describe('Utils', function () { } }); - it('includes the correct field name', function () { - const filePath = path.join(__dirname, 'fixtures/test-file.txt'); - const formData = attachFile('myfield', filePath); + it("includes the correct field name", function () { + const filePath = path.join(__dirname, "fixtures/test-file.txt"); + const formData = attachFile("myfield", filePath); const buffer = formData.getBuffer(); const content = buffer.toString(); @@ -107,9 +107,9 @@ describe('Utils', function () { assert.match(content, /Content-Disposition: form-data; name="myfield"/); }); - it('includes the file content', function () { - const filePath = path.join(__dirname, 'fixtures/test-file.txt'); - const formData = attachFile('testfile', filePath); + it("includes the file content", function () { + const filePath = path.join(__dirname, "fixtures/test-file.txt"); + const formData = attachFile("testfile", filePath); const buffer = formData.getBuffer(); const content = buffer.toString(); @@ -118,15 +118,15 @@ describe('Utils', function () { assert.match(content, /test content for file upload/); }); - it('can append to existing FormData', function () { - const filePath1 = path.join(__dirname, 'fixtures/test-file.txt'); - const filePath2 = path.join(__dirname, 'fixtures/ghost-favicon.png'); + it("can append to existing FormData", function () { + const filePath1 = path.join(__dirname, "fixtures/test-file.txt"); + const filePath2 = path.join(__dirname, "fixtures/ghost-favicon.png"); // Create FormData with first file - const formData = attachFile('file1', filePath1); + const formData = attachFile("file1", filePath1); // Append second file to same FormData - const updatedFormData = attachFile('file2', filePath2, formData); + const updatedFormData = attachFile("file2", filePath2, formData); // Should be the same instance assert.equal(formData, updatedFormData); @@ -141,9 +141,9 @@ describe('Utils', function () { assert.match(content, /filename="ghost-favicon.png"/); }); - it('creates new FormData when existingFormData is null', function () { - const filePath = path.join(__dirname, 'fixtures/test-file.txt'); - const formData = attachFile('testfile', filePath, null); + it("creates new FormData when existingFormData is null", function () { + const filePath = path.join(__dirname, "fixtures/test-file.txt"); + const formData = attachFile("testfile", filePath, null); assert.equal(formData instanceof FormData, true); diff --git a/packages/express-test/test/utils/index.js b/packages/express-test/test/utils/index.js index 804550a89..e47e813b9 100644 --- a/packages/express-test/test/utils/index.js +++ b/packages/express-test/test/utils/index.js @@ -4,17 +4,17 @@ * Shared utils for writing tests */ -const sinon = require('sinon'); +const sinon = require("sinon"); const stubCookies = (request) => { - const saveCookiesStub = request._saveCookies = sinon.stub(); - const restoreCookiesStub = request._restoreCookies = sinon.stub(); - return {saveCookiesStub, restoreCookiesStub}; + const saveCookiesStub = (request._saveCookies = sinon.stub()); + const restoreCookiesStub = (request._restoreCookies = sinon.stub()); + return { saveCookiesStub, restoreCookiesStub }; }; // Require overrides - these add globals for tests module.exports = { sinon, - assert: require('assert'), - stubCookies + assert: require("assert"), + stubCookies, }; diff --git a/packages/express-test/test/utils/overrides.js b/packages/express-test/test/utils/overrides.js index 77dcb5dd7..27090e9c4 100644 --- a/packages/express-test/test/utils/overrides.js +++ b/packages/express-test/test/utils/overrides.js @@ -1,11 +1,13 @@ -const {snapshotManager} = require('@tryghost/jest-snapshot'); +const { snapshotManager } = require("@tryghost/jest-snapshot"); /* eslint-disable ghost/mocha/no-mocha-arrows, ghost/mocha/no-top-level-hooks, ghost/mocha/handle-done-callback */ -beforeAll(() => { // eslint-disable-line no-undef +beforeAll(() => { + // eslint-disable-line no-undef snapshotManager.resetRegistry(); }); -beforeEach((context) => { // eslint-disable-line no-undef +beforeEach((context) => { + // eslint-disable-line no-undef // Reconstruct full title similar to mocha's fullTitle() const parts = []; let suite = context.task.suite; @@ -17,6 +19,6 @@ beforeEach((context) => { // eslint-disable-line no-undef snapshotManager.setCurrentTest({ testPath: context.task.file.filepath, - testTitle: parts.join(' ') + testTitle: parts.join(" "), }); }); diff --git a/packages/express-test/vitest.config.ts b/packages/express-test/vitest.config.ts index c69603f55..866fadb05 100644 --- a/packages/express-test/vitest.config.ts +++ b/packages/express-test/vitest.config.ts @@ -1,10 +1,13 @@ -import {defineConfig, mergeConfig} from 'vitest/config'; -import rootConfig from '../../vitest.config'; +import { defineConfig, mergeConfig } from "vitest/config"; +import rootConfig from "../../vitest.config"; // Override: setupFiles needed to initialize the snapshot test registry // before each test (replaces Mocha's --require flag). -export default mergeConfig(rootConfig, defineConfig({ - test: { - setupFiles: ['./test/utils/overrides.js'] - } -})); +export default mergeConfig( + rootConfig, + defineConfig({ + test: { + setupFiles: ["./test/utils/overrides.js"], + }, + }), +); diff --git a/packages/http-cache-utils/README.md b/packages/http-cache-utils/README.md index 215fe7372..c3d01272d 100644 --- a/packages/http-cache-utils/README.md +++ b/packages/http-cache-utils/README.md @@ -16,24 +16,20 @@ Utilities for cache-control logic, including user-specific response detection an ## Usage - ## Develop This is a monorepo package. Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests +# Copyright & License - -# Copyright & License - -Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/http-cache-utils/index.js b/packages/http-cache-utils/index.js index de142b0e1..fba48f830 100644 --- a/packages/http-cache-utils/index.js +++ b/packages/http-cache-utils/index.js @@ -1 +1 @@ -module.exports = require('./lib/http-cache-utils'); +module.exports = require("./lib/http-cache-utils"); diff --git a/packages/http-cache-utils/lib/http-cache-utils.js b/packages/http-cache-utils/lib/http-cache-utils.js index 14224728f..b00a8b9c5 100644 --- a/packages/http-cache-utils/lib/http-cache-utils.js +++ b/packages/http-cache-utils/lib/http-cache-utils.js @@ -4,34 +4,33 @@ * @returns Boolean */ module.exports.isReqResUserSpecific = (req, res) => { - return req?.get('cookie') - || req?.get('authorization') - || res?.get('set-cookie'); + return req?.get("cookie") || req?.get("authorization") || res?.get("set-cookie"); }; /** * Kitchen sink of Cache-Control header values used in Ghost - * + * * Reference of value meanings (based on rfc9111 - https://httpwg.org/specs/rfc9111.html): - * + * * 'no-cache' - The response MUST NOT be used to satisfy any other request without * forwarding it for validation and receiving a successful response. - * - * 'private' - Indicates that a shared cache MUST NOT store the response (i.e., the response + * + * 'private' - Indicates that a shared cache MUST NOT store the response (i.e., the response * is intended for a single user). * In context of Ghost it means the header should only be used if there are * cookie or authorization headers set on the response, otherwise there’s no * “single user” intention. - * + * * 'no-store' - A cache MUST NOT store any part of either the immediate request or the * response and MUST NOT use the response to satisfy any other request. - * + * * 'must-revalidate'- Means that the response must not be reused without revalidation once it is stale. - * + * */ module.exports.cacheControlValues = { // never cache a single bit in any type of cache - private: 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0', + private: "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", // never cache except if it's a shared cache (lack of 'private' allows to do so) - noCacheDynamic: 'no-cache, max-age=0, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0' + noCacheDynamic: + "no-cache, max-age=0, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", }; diff --git a/packages/http-cache-utils/package.json b/packages/http-cache-utils/package.json index c6bdf9590..d82c6252c 100644 --- a/packages/http-cache-utils/package.json +++ b/packages/http-cache-utils/package.json @@ -1,14 +1,21 @@ { "name": "@tryghost/http-cache-utils", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/http-cache-utils" }, - "author": "Ghost Foundation", - "license": "MIT", + "files": [ + "index.js", + "lib" + ], "main": "index.js", + "publishConfig": { + "access": "public" + }, "scripts": { "dev": "echo \"Implement me!\"", "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", @@ -18,13 +25,6 @@ "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", "lint:eslint": "yarn lint:code && yarn lint:test" }, - "files": [ - "index.js", - "lib" - ], - "publishConfig": { - "access": "public" - }, "devDependencies": { "sinon": "21.0.3" } diff --git a/packages/http-cache-utils/test/.eslintrc.js b/packages/http-cache-utils/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/http-cache-utils/test/.eslintrc.js +++ b/packages/http-cache-utils/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/http-cache-utils/test/http-cache-utils.test.js b/packages/http-cache-utils/test/http-cache-utils.test.js index 06ba70346..bbb5c4a9c 100644 --- a/packages/http-cache-utils/test/http-cache-utils.test.js +++ b/packages/http-cache-utils/test/http-cache-utils.test.js @@ -1,76 +1,74 @@ -const assert = require('assert/strict'); +const assert = require("assert/strict"); -const {isReqResUserSpecific} = require('../'); +const { isReqResUserSpecific } = require("../"); -describe('Cache Utils', function () { - describe('isReqResUserSpecific', function () { - it('returns FALSY result for request/response pair containing NO user-identifying parameters', function () { +describe("Cache Utils", function () { + describe("isReqResUserSpecific", function () { + it("returns FALSY result for request/response pair containing NO user-identifying parameters", function () { let req, res; req = { get() { return false; - } + }, }; res = { get() { return false; - } + }, }; assert.equal(false, isReqResUserSpecific(req, res)); }); - it('returns TRUTHY result for request/response pair containing user-identifying parameters', function () { + it("returns TRUTHY result for request/response pair containing user-identifying parameters", function () { let req, res; req = { get() { return false; - } + }, }; res = { get(header) { - if (header === 'set-cookie') { - return 'maui:cafebabe; MaxAge=42'; + if (header === "set-cookie") { + return "maui:cafebabe; MaxAge=42"; } - } + }, }; assert(isReqResUserSpecific(req, res)); req = { get(header) { - if (header === 'cookie') { - return 'PHPSESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1'; + if (header === "cookie") { + return "PHPSESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1"; } - } - + }, }; res = { get() { return false; - } + }, }; assert(isReqResUserSpecific(req, res)); req = { get(header) { - if (header === 'authorization') { - return 'Basic YWxhZGRpbjpvcGVuc2VzYW1l'; + if (header === "authorization") { + return "Basic YWxhZGRpbjpvcGVuc2VzYW1l"; } - } - + }, }; res = { get() { return false; - } + }, }; assert(isReqResUserSpecific(req, res)); diff --git a/packages/http-stream/README.md b/packages/http-stream/README.md index 8df804fa4..212ff2a3f 100644 --- a/packages/http-stream/README.md +++ b/packages/http-stream/README.md @@ -10,36 +10,30 @@ or `yarn add @tryghost/http-stream` - ## Purpose HTTP response streaming helper used for piping remote/local resources through Ghost responses. ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests +# Copyright & License - - -# Copyright & License - -Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/http-stream/index.js b/packages/http-stream/index.js index 26e0cd467..200002521 100644 --- a/packages/http-stream/index.js +++ b/packages/http-stream/index.js @@ -1 +1 @@ -module.exports = require('./lib/HttpStream'); +module.exports = require("./lib/HttpStream"); diff --git a/packages/http-stream/lib/HttpStream.js b/packages/http-stream/lib/HttpStream.js index 0bebe100e..47f6c1c35 100644 --- a/packages/http-stream/lib/HttpStream.js +++ b/packages/http-stream/lib/HttpStream.js @@ -1,6 +1,6 @@ -const request = require('@tryghost/request'); -const debug = require('debug')('@tryghost/http-stream'); -const GhostError = require('@tryghost/errors'); +const request = require("@tryghost/request"); +const debug = require("debug")("@tryghost/http-stream"); +const GhostError = require("@tryghost/errors"); class HttpStream { constructor(config) { @@ -9,21 +9,23 @@ class HttpStream { async write(data) { try { - if (typeof data !== 'object') { - throw new GhostError.IncorrectUsageError({message: 'Type Error: Http transport requires log data to be an object'}); + if (typeof data !== "object") { + throw new GhostError.IncorrectUsageError({ + message: "Type Error: Http transport requires log data to be an object", + }); } const options = { ...this.config, - method: 'POST', - json: data + method: "POST", + json: data, }; - const {url} = options; + const { url } = options; delete options.url; return await request(url, options); } catch (error) { - debug('Failed to ship log', error.message); + debug("Failed to ship log", error.message); return false; } } diff --git a/packages/http-stream/package.json b/packages/http-stream/package.json index 006bf0342..59f75eac3 100644 --- a/packages/http-stream/package.json +++ b/packages/http-stream/package.json @@ -1,14 +1,21 @@ { "name": "@tryghost/http-stream", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/http-stream" }, - "author": "Ghost Foundation", - "license": "MIT", + "files": [ + "index.js", + "lib" + ], "main": "index.js", + "publishConfig": { + "access": "public" + }, "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", @@ -18,19 +25,12 @@ "posttest": "yarn lint", "lint:eslint": "yarn lint:code && yarn lint:test" }, - "files": [ - "index.js", - "lib" - ], - "publishConfig": { - "access": "public" + "dependencies": { + "@tryghost/errors": "^3.0.3", + "@tryghost/request": "^3.0.3" }, "devDependencies": { "express": "5.2.1", "sinon": "21.0.3" - }, - "dependencies": { - "@tryghost/errors": "^3.0.3", - "@tryghost/request": "^3.0.3" } } diff --git a/packages/http-stream/test/.eslintrc.js b/packages/http-stream/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/http-stream/test/.eslintrc.js +++ b/packages/http-stream/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/http-stream/test/HttpStream.test.js b/packages/http-stream/test/HttpStream.test.js index 51f221044..d14189d55 100644 --- a/packages/http-stream/test/HttpStream.test.js +++ b/packages/http-stream/test/HttpStream.test.js @@ -1,26 +1,26 @@ -const assert = require('assert/strict'); -const sinon = require('sinon'); +const assert = require("assert/strict"); +const sinon = require("sinon"); -const requestPath = require.resolve('@tryghost/request'); +const requestPath = require.resolve("@tryghost/request"); const originalRequest = require(requestPath); -const indexPath = require.resolve('../index'); -const httpStreamPath = require.resolve('../lib/HttpStream'); +const indexPath = require.resolve("../index"); +const httpStreamPath = require.resolve("../lib/HttpStream"); let HttpStream; let requestStub; const testConfig = { - url: 'http://127.0.0.1:3001' + url: "http://127.0.0.1:3001", }; -describe('HttpStream', function () { - describe('write', function () { +describe("HttpStream", function () { + describe("write", function () { beforeEach(function () { requestStub = sinon.stub(); require.cache[requestPath].exports = requestStub; delete require.cache[indexPath]; delete require.cache[httpStreamPath]; - HttpStream = require('../index'); + HttpStream = require("../index"); }); afterEach(function () { @@ -30,36 +30,36 @@ describe('HttpStream', function () { delete require.cache[httpStreamPath]; }); - it('return false when string passed', async function () { + it("return false when string passed", async function () { const stream = new HttpStream(testConfig); - const res = await stream.write('this shouldnt work'); + const res = await stream.write("this shouldnt work"); assert.equal(res, false); sinon.assert.notCalled(requestStub); }); - it('be successful when object passed', async function () { + it("be successful when object passed", async function () { const expectedRes = { statusCode: 200, - body: '{"this":"should work"}' + body: '{"this":"should work"}', }; requestStub.resolves(expectedRes); const stream = new HttpStream(testConfig); - const body = {this: 'should work'}; + const body = { this: "should work" }; const res = await stream.write(body); sinon.assert.calledOnceWithExactly(requestStub, testConfig.url, { - method: 'POST', - json: body + method: "POST", + json: body, }); assert.deepEqual(res, expectedRes); }); - it('return false when request fails', async function () { - requestStub.rejects(new Error('request failed')); + it("return false when request fails", async function () { + requestStub.rejects(new Error("request failed")); const stream = new HttpStream(testConfig); - const res = await stream.write({this: 'should fail'}); + const res = await stream.write({ this: "should fail" }); assert.equal(res, false); }); diff --git a/packages/jest-snapshot/.eslintrc.js b/packages/jest-snapshot/.eslintrc.js index 371fec441..78b9d4c9b 100644 --- a/packages/jest-snapshot/.eslintrc.js +++ b/packages/jest-snapshot/.eslintrc.js @@ -1,9 +1,5 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/node' - ], - ignorePatterns: [ - 'coverage' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/node"], + ignorePatterns: ["coverage"], }; diff --git a/packages/jest-snapshot/README.md b/packages/jest-snapshot/README.md index 163bb88cd..bb5b7ad36 100644 --- a/packages/jest-snapshot/README.md +++ b/packages/jest-snapshot/README.md @@ -10,36 +10,30 @@ or `yarn add @tryghost/jest-snapshot` - ## Purpose Snapshot assertion utilities and state management for Jest-style snapshots in non-Jest test suites. ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests +# Copyright & License - - -# Copyright & License - -Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/jest-snapshot/index.js b/packages/jest-snapshot/index.js index fcbf31b1c..66d3d7839 100644 --- a/packages/jest-snapshot/index.js +++ b/packages/jest-snapshot/index.js @@ -1 +1 @@ -module.exports = require('./lib/jest-snapshot'); +module.exports = require("./lib/jest-snapshot"); diff --git a/packages/jest-snapshot/lib/SnapshotManager.js b/packages/jest-snapshot/lib/SnapshotManager.js index d4700beae..6f58bd31e 100644 --- a/packages/jest-snapshot/lib/SnapshotManager.js +++ b/packages/jest-snapshot/lib/SnapshotManager.js @@ -1,9 +1,9 @@ -const {SnapshotState, toMatchSnapshot, EXTENSION} = require('jest-snapshot'); -const errors = require('@tryghost/errors'); -const utils = require('@jest/expect-utils'); -const assert = require('assert'); -const path = require('path'); -const {makeMessageFromMatchMessage} = require('./utils'); +const { SnapshotState, toMatchSnapshot, EXTENSION } = require("jest-snapshot"); +const errors = require("@tryghost/errors"); +const utils = require("@jest/expect-utils"); +const assert = require("assert"); +const path = require("path"); +const { makeMessageFromMatchMessage } = require("./utils"); const DOT_EXTENSION = `.${EXTENSION}`; @@ -12,21 +12,20 @@ const DOT_EXTENSION = `.${EXTENSION}`; * @returns {string} e.g. 'all' or 'new' */ function willUpdate() { - const updateSnapshots = ( - process.env.SNAPSHOT_UPDATE - || process.env.UPDATE_SNAPSHOT - || process.env.SNAPSHOTS_UPDATE - || process.env.UPDATE_SNAPSHOTS - ); - - return updateSnapshots ? 'all' : 'new'; + const updateSnapshots = + process.env.SNAPSHOT_UPDATE || + process.env.UPDATE_SNAPSHOT || + process.env.SNAPSHOTS_UPDATE || + process.env.UPDATE_SNAPSHOTS; + + return updateSnapshots ? "all" : "new"; } class SnapshotManager { constructor() { this.registry = {}; this.currentTest = {}; - this.defaultSnapshotRoot = '__snapshots__'; + this.defaultSnapshotRoot = "__snapshots__"; // Set willUpdate once, as it can't change whilst we are running this.willUpdate = willUpdate(); @@ -61,7 +60,7 @@ class SnapshotManager { _resolveSnapshotFilePath(testPath) { return path.join( path.join(path.dirname(testPath), this.defaultSnapshotRoot), - path.basename(testPath) + DOT_EXTENSION + path.basename(testPath) + DOT_EXTENSION, ); } @@ -73,18 +72,19 @@ class SnapshotManager { _getConfig() { if (!this.currentTest.testPath || !this.currentTest.testTitle) { throw new errors.IncorrectUsageError({ - message: 'Unable to run snapshot tests, current test was not configured', - context: 'Snapshot testing requires current test filename and nameTemplate to be set for each test', - help: 'Did you forget to do export.mochaHooks?' + message: "Unable to run snapshot tests, current test was not configured", + context: + "Snapshot testing requires current test filename and nameTemplate to be set for each test", + help: "Did you forget to do export.mochaHooks?", }); } - const {testPath, testTitle} = this.currentTest; + const { testPath, testTitle } = this.currentTest; const snapshotName = this._getNameForSnapshot(testPath, testTitle); const snapshotPath = this._resolveSnapshotFilePath(testPath); - return {snapshotPath, snapshotName}; + return { snapshotPath, snapshotName }; } /** @@ -98,7 +98,7 @@ class SnapshotManager { * Resets the registry for the current test only */ resetRegistryForCurrentTest() { - const {testPath, testTitle} = this.currentTest; + const { testPath, testTitle } = this.currentTest; if (testPath in this.registry && testTitle in this.registry[testPath]) { delete this.registry[testPath][testTitle]; } @@ -122,7 +122,7 @@ class SnapshotManager { // Initialize the SnapshotState, it’s responsible for actually matching // actual snapshot with expected one and storing results return new SnapshotState(snapshotPath, { - updateSnapshot: this.willUpdate + updateSnapshot: this.willUpdate, }); } @@ -134,12 +134,12 @@ class SnapshotManager { * @param {{properties: Object, field: string, error: Object, hint: string}} assertion */ assertSnapshot(response, assertion) { - const {properties, field, error} = assertion; + const { properties, field, error } = assertion; if (!response[field]) { error.message = `Unable to match snapshot on undefined field ${field} ${error.contextString}`; error.expected = field; - error.actual = 'undefined'; + error.actual = "undefined"; assert.notEqual(response[field], undefined, error); } @@ -151,7 +151,7 @@ class SnapshotManager { const errorMessage = `"response.${field}" is missing the expected property "${prop}"`; error.message = makeMessageFromMatchMessage(match.message(), errorMessage); error.expected = prop; - error.actual = 'undefined'; + error.actual = "undefined"; error.showDiff = false; // Disable mocha's diff output as it's already present in match.message() assert.notEqual(response[field][prop], undefined, error); @@ -181,7 +181,7 @@ class SnapshotManager { * @returns {Object} result of the match */ match(received, properties = {}, hint) { - const {snapshotPath, snapshotName} = this._getConfig(); + const { snapshotPath, snapshotName } = this._getConfig(); const snapshotState = this.getSnapshotState(snapshotPath); @@ -189,7 +189,7 @@ class SnapshotManager { snapshotState, currentTestName: snapshotName, utils, - equals: utils.equals + equals: utils.equals, }); // Execute the matcher diff --git a/packages/jest-snapshot/lib/jest-snapshot.js b/packages/jest-snapshot/lib/jest-snapshot.js index e90f4823c..4c1480fcd 100644 --- a/packages/jest-snapshot/lib/jest-snapshot.js +++ b/packages/jest-snapshot/lib/jest-snapshot.js @@ -1,9 +1,9 @@ -const {jestExpect} = require('@jest/expect'); -const SnapshotManager = require('./SnapshotManager'); +const { jestExpect } = require("@jest/expect"); +const SnapshotManager = require("./SnapshotManager"); const snapshotManager = new SnapshotManager(); function matchSnapshotAssertion(properties) { - this.params = {operator: 'to match a stored snapshot'}; + this.params = { operator: "to match a stored snapshot" }; const result = snapshotManager.match(this.obj, properties); @@ -12,7 +12,6 @@ function matchSnapshotAssertion(properties) { } const mochaHooks = { - /** * Runs before all tests * Resets the registry so we start with a clean slate @@ -27,7 +26,7 @@ const mochaHooks = { * If we're running a retry, reset the registry for this test only */ beforeEach() { - const {currentTest} = this; + const { currentTest } = this; if (currentTest.currentRetry() > 0) { // Reset registry for this test only @@ -36,12 +35,12 @@ const mochaHooks = { snapshotManager.setCurrentTest({ testPath: currentTest.file, - testTitle: currentTest.fullTitle() + testTitle: currentTest.fullTitle(), }); - } + }, }; -const {any, anything, stringMatching} = jestExpect; +const { any, anything, stringMatching } = jestExpect; module.exports = { mochaHooks, @@ -49,5 +48,5 @@ module.exports = { matchSnapshotAssertion, any, anything, - stringMatching + stringMatching, }; diff --git a/packages/jest-snapshot/lib/utils.js b/packages/jest-snapshot/lib/utils.js index 1913af157..9bcc9fe3d 100644 --- a/packages/jest-snapshot/lib/utils.js +++ b/packages/jest-snapshot/lib/utils.js @@ -4,8 +4,11 @@ * @param {String} errorMessage * @returns {String} */ -module.exports.makeMessageFromMatchMessage = function makeMessageFromMatchMessage(message, errorMessage) { - const messageLines = message.split('\n'); +module.exports.makeMessageFromMatchMessage = function makeMessageFromMatchMessage( + message, + errorMessage, +) { + const messageLines = message.split("\n"); messageLines.splice(0, 1, errorMessage); - return messageLines.join('\n'); + return messageLines.join("\n"); }; diff --git a/packages/jest-snapshot/package.json b/packages/jest-snapshot/package.json index 39766bfa9..3a9a8c362 100644 --- a/packages/jest-snapshot/package.json +++ b/packages/jest-snapshot/package.json @@ -1,35 +1,35 @@ { "name": "@tryghost/jest-snapshot", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/jest-snapshot" }, - "author": "Ghost Foundation", - "license": "MIT", - "main": "index.js", - "scripts": { - "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" - }, "files": [ "index.js", "lib" ], + "main": "index.js", "publishConfig": { "access": "public" }, - "devDependencies": { - "sinon": "21.0.3" + "scripts": { + "dev": "echo \"Implement me!\"", + "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "dependencies": { "@jest/expect": "30.3.0", "@jest/expect-utils": "30.3.0", "@tryghost/errors": "^3.0.3", "jest-snapshot": "30.3.0" + }, + "devDependencies": { + "sinon": "21.0.3" } } diff --git a/packages/jest-snapshot/test/.eslintrc.js b/packages/jest-snapshot/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/jest-snapshot/test/.eslintrc.js +++ b/packages/jest-snapshot/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/jest-snapshot/test/SnapshotManager.test.js b/packages/jest-snapshot/test/SnapshotManager.test.js index 255646fd3..d1a3bcebd 100644 --- a/packages/jest-snapshot/test/SnapshotManager.test.js +++ b/packages/jest-snapshot/test/SnapshotManager.test.js @@ -1,28 +1,28 @@ -const {assert, sinon} = require('./utils'); +const { assert, sinon } = require("./utils"); -const SnapshotManager = require('../lib/SnapshotManager'); +const SnapshotManager = require("../lib/SnapshotManager"); -describe('Snapshot Manager', function () { +describe("Snapshot Manager", function () { afterEach(function () { sinon.restore(); }); - it('can create a new instance', function () { + it("can create a new instance", function () { const snapshotManager = new SnapshotManager(); assert.deepEqual(snapshotManager.registry, {}); assert.deepEqual(snapshotManager.currentTest, {}); - assert.equal(snapshotManager.willUpdate, 'new'); + assert.equal(snapshotManager.willUpdate, "new"); }); - it('resetRegistry: will empty the registry when called', function () { + it("resetRegistry: will empty the registry when called", function () { const snapshotManager = new SnapshotManager(); assert.deepEqual(snapshotManager.registry, {}); snapshotManager.registry = { - 'test/my-fake.test.js': { + "test/my-fake.test.js": { bar: 1, - foo: 2 - } + foo: 2, + }, }; snapshotManager.resetRegistry(); @@ -30,82 +30,88 @@ describe('Snapshot Manager', function () { assert.deepEqual(snapshotManager.registry, {}); }); - it('resetRegistry: will not throw if no registry exists', function () { + it("resetRegistry: will not throw if no registry exists", function () { const snapshotManager = new SnapshotManager(); assert.doesNotThrow(() => snapshotManager.resetRegistry()); }); - it('resetRegistryForCurrentTest: will empty the registry for the current test when called', function () { + it("resetRegistryForCurrentTest: will empty the registry for the current test when called", function () { const snapshotManager = new SnapshotManager(); assert.deepEqual(snapshotManager.registry, {}); snapshotManager.registry = { - 'test/my-fake.test.js': { + "test/my-fake.test.js": { bar: 1, - foo: 2 - } + foo: 2, + }, }; - snapshotManager.setCurrentTest({testPath: 'test/my-fake.test.js', testTitle: 'foo'}); + snapshotManager.setCurrentTest({ testPath: "test/my-fake.test.js", testTitle: "foo" }); snapshotManager.resetRegistryForCurrentTest(); - assert.deepEqual(snapshotManager.registry, {'test/my-fake.test.js': {bar: 1}}); + assert.deepEqual(snapshotManager.registry, { "test/my-fake.test.js": { bar: 1 } }); }); - it('resetRegistryForCurrentTest: will not throw if no registry exists for current test', function () { + it("resetRegistryForCurrentTest: will not throw if no registry exists for current test", function () { const snapshotManager = new SnapshotManager(); - snapshotManager.setCurrentTest({testPath: 'test/my-fake.test.js', testTitle: 'foo'}); + snapshotManager.setCurrentTest({ testPath: "test/my-fake.test.js", testTitle: "foo" }); - assert.doesNotThrow(() => snapshotManager.resetRegistryForCurrentTest(), 'should not throw if no registry exists for current test'); + assert.doesNotThrow( + () => snapshotManager.resetRegistryForCurrentTest(), + "should not throw if no registry exists for current test", + ); }); - it('setCurrentTest: results in currentTest being set', function () { + it("setCurrentTest: results in currentTest being set", function () { const snapshotManager = new SnapshotManager(); assert.deepEqual(snapshotManager.currentTest, {}); - snapshotManager.setCurrentTest({foo: 'bar'}); - assert.deepEqual(snapshotManager.currentTest, {foo: 'bar'}); + snapshotManager.setCurrentTest({ foo: "bar" }); + assert.deepEqual(snapshotManager.currentTest, { foo: "bar" }); }); - it('_getNameForSnapshot: will increment the counter for each snapshot name correctly', function () { + it("_getNameForSnapshot: will increment the counter for each snapshot name correctly", function () { const snapshotManager = new SnapshotManager(); assert.equal( - snapshotManager._getNameForSnapshot('test/my-fake.test.js', 'testing bar'), - 'testing bar 1' + snapshotManager._getNameForSnapshot("test/my-fake.test.js", "testing bar"), + "testing bar 1", ); assert.equal( - snapshotManager._getNameForSnapshot('test/my-fake.test.js', 'testing baz'), - 'testing baz 1' + snapshotManager._getNameForSnapshot("test/my-fake.test.js", "testing baz"), + "testing baz 1", ); assert.equal( - snapshotManager._getNameForSnapshot('test/my-fake.test.js', 'testing bar'), - 'testing bar 2' + snapshotManager._getNameForSnapshot("test/my-fake.test.js", "testing bar"), + "testing bar 2", ); }); - it('_resolveSnapshotFilePath: will resolve the snapshot file path correctly', function () { + it("_resolveSnapshotFilePath: will resolve the snapshot file path correctly", function () { const snapshotManager = new SnapshotManager(); // Fake path with test file inside test folder - let inputPath = '/full/path/to/tests/foo.js'; + let inputPath = "/full/path/to/tests/foo.js"; let outputPath = snapshotManager._resolveSnapshotFilePath(inputPath); - assert.equal(outputPath, '/full/path/to/tests/__snapshots__/foo.js.snap'); + assert.equal(outputPath, "/full/path/to/tests/__snapshots__/foo.js.snap"); // Fake path with test file nested beneath test folder - inputPath = '/full/path/to/tests/unit/foo.js'; + inputPath = "/full/path/to/tests/unit/foo.js"; outputPath = snapshotManager._resolveSnapshotFilePath(inputPath); - assert.equal(outputPath, '/full/path/to/tests/unit/__snapshots__/foo.js.snap'); + assert.equal(outputPath, "/full/path/to/tests/unit/__snapshots__/foo.js.snap"); // Real example using current test file path inputPath = __filename; outputPath = snapshotManager._resolveSnapshotFilePath(inputPath); - assert.match(outputPath, /\/packages\/jest-snapshot\/test\/__snapshots__\/SnapshotManager\.test\.js\.snap/); + assert.match( + outputPath, + /\/packages\/jest-snapshot\/test\/__snapshots__\/SnapshotManager\.test\.js\.snap/, + ); }); - it('_resolveSnapshotFilePath: resolve snapshot files paths exactly the same as jest', function () { + it("_resolveSnapshotFilePath: resolve snapshot files paths exactly the same as jest", function () { const snapshotManager = new SnapshotManager(); // https://github.com/jestjs/jest/blob/main/packages/jest-snapshot/src/__tests__/SnapshotResolver.test.ts#L32-L36 @@ -113,27 +119,27 @@ describe('Snapshot Manager', function () { // path.join('/abc', 'cde', '__snapshots__', 'a.test.js.snap'), // ); - const result = snapshotManager._resolveSnapshotFilePath('/abc/cde/a.test.js'); - assert.equal(result, '/abc/cde/__snapshots__/a.test.js.snap'); + const result = snapshotManager._resolveSnapshotFilePath("/abc/cde/a.test.js"); + assert.equal(result, "/abc/cde/__snapshots__/a.test.js.snap"); }); - it('_willUpdate: will return all when the environment variable is set', function () { - const originalValue = process.env.SNAPSHOT_UPDATE || ''; + it("_willUpdate: will return all when the environment variable is set", function () { + const originalValue = process.env.SNAPSHOT_UPDATE || ""; process.env.SNAPSHOT_UPDATE = 1; const snapshotManager = new SnapshotManager(); - assert.equal(snapshotManager.willUpdate, 'all'); + assert.equal(snapshotManager.willUpdate, "all"); process.env.SNAPSHOT_UPDATE = originalValue; }); - it('_willUpdate: will return new when the environment variable is not set', function () { - const originalValue = process.env.SNAPSHOT_UPDATE || ''; - process.env.SNAPSHOT_UPDATE = ''; + it("_willUpdate: will return new when the environment variable is not set", function () { + const originalValue = process.env.SNAPSHOT_UPDATE || ""; + process.env.SNAPSHOT_UPDATE = ""; const snapshotManager = new SnapshotManager(); - assert.equal(snapshotManager.willUpdate, 'new'); + assert.equal(snapshotManager.willUpdate, "new"); process.env.SNAPSHOT_UPDATE = originalValue; }); - it('_getConfig: will throw if no current test is set', function () { + it("_getConfig: will throw if no current test is set", function () { const snapshotManager = new SnapshotManager(); // If there's no currentTest... @@ -141,35 +147,37 @@ describe('Snapshot Manager', function () { snapshotManager._getConfig(); }; - assert.throws(assertFn, {message: 'Unable to run snapshot tests, current test was not configured'}); + assert.throws(assertFn, { + message: "Unable to run snapshot tests, current test was not configured", + }); }); - it('_getConfig: will return the correct config when a current test is set', function () { + it("_getConfig: will return the correct config when a current test is set", function () { const snapshotManager = new SnapshotManager(); - let nameSpy = sinon.spy(snapshotManager, '_getNameForSnapshot'); + let nameSpy = sinon.spy(snapshotManager, "_getNameForSnapshot"); snapshotManager.setCurrentTest({ - testPath: 'test/my-fake.test.js', - testTitle: 'My fake test title' + testPath: "test/my-fake.test.js", + testTitle: "My fake test title", }); let config = snapshotManager._getConfig(); - assert.equal(config.snapshotPath, 'test/__snapshots__/my-fake.test.js.snap'); - assert.equal(config.snapshotName, 'My fake test title 1'); + assert.equal(config.snapshotPath, "test/__snapshots__/my-fake.test.js.snap"); + assert.equal(config.snapshotName, "My fake test title 1"); sinon.assert.calledOnce(nameSpy); }); - describe('assert snapshot', function () { - it('ok when match is a pass', function () { + describe("assert snapshot", function () { + it("ok when match is a pass", function () { const snapshotManager = new SnapshotManager(); - const matchStub = sinon.stub(snapshotManager, 'match').returns({pass: true}); + const matchStub = sinon.stub(snapshotManager, "match").returns({ pass: true }); const error = new assert.AssertionError({}); - error.contextString = 'foo'; + error.contextString = "foo"; - const response = {body: {foo: 'bar'}}; - const assertion = {properties: {}, field: 'body', error}; + const response = { body: { foo: "bar" } }; + const assertion = { properties: {}, field: "body", error }; const assertFn = () => { snapshotManager.assertSnapshot(response, assertion); @@ -179,21 +187,26 @@ describe('Snapshot Manager', function () { // Assert side effects, check that hinting works as expected sinon.assert.calledOnce(matchStub); - sinon.assert.calledOnceWithExactly(matchStub, response.body, {}, '[body]'); + sinon.assert.calledOnceWithExactly(matchStub, response.body, {}, "[body]"); }); - it('ok when match is a with extra properties', function () { + it("ok when match is a with extra properties", function () { const snapshotManager = new SnapshotManager(); - const matchStub = sinon.stub(snapshotManager, 'match').returns({ - message: () => 'hello', - pass: true + const matchStub = sinon.stub(snapshotManager, "match").returns({ + message: () => "hello", + pass: true, }); const error = new assert.AssertionError({}); - error.contextString = 'foo'; - - const response = {body: {foo: 'bar'}}; - const assertion = {properties: {foo: 'bar'}, field: 'body', hint: '[custom hint]', error}; + error.contextString = "foo"; + + const response = { body: { foo: "bar" } }; + const assertion = { + properties: { foo: "bar" }, + field: "body", + hint: "[custom hint]", + error, + }; const assertFn = () => { snapshotManager.assertSnapshot(response, assertion); @@ -203,18 +216,23 @@ describe('Snapshot Manager', function () { // Assert side effects, check that custom hinting works as expected sinon.assert.calledOnce(matchStub); - sinon.assert.calledOnceWithExactly(matchStub, response.body, {foo: 'bar'}, '[custom hint]'); + sinon.assert.calledOnceWithExactly( + matchStub, + response.body, + { foo: "bar" }, + "[custom hint]", + ); }); - it('not ok when match is not a pass', function () { + it("not ok when match is not a pass", function () { const snapshotManager = new SnapshotManager(); - sinon.stub(snapshotManager, 'match').returns({pass: false}); + sinon.stub(snapshotManager, "match").returns({ pass: false }); const error = new assert.AssertionError({}); - error.contextString = 'foo'; + error.contextString = "foo"; - const response = {body: {foo: 'bar'}}; - const assertion = {properties: {}, field: 'body', error}; + const response = { body: { foo: "bar" } }; + const assertion = { properties: {}, field: "body", error }; const assertFn = () => { snapshotManager.assertSnapshot(response, assertion); @@ -223,75 +241,77 @@ describe('Snapshot Manager', function () { assert.throws(assertFn); }); - it('not ok when field not set', function () { + it("not ok when field not set", function () { const snapshotManager = new SnapshotManager(); - sinon.stub(snapshotManager, 'match').returns({pass: false}); + sinon.stub(snapshotManager, "match").returns({ pass: false }); const error = new assert.AssertionError({}); - error.contextString = 'foo'; + error.contextString = "foo"; - const response = {body: {foo: 'bar'}}; - const assertion = {properties: {}, error}; + const response = { body: { foo: "bar" } }; + const assertion = { properties: {}, error }; const assertFn = () => { snapshotManager.assertSnapshot(response, assertion); }; - assert.throws(assertFn, {message: 'Unable to match snapshot on undefined field undefined foo'}); + assert.throws(assertFn, { + message: "Unable to match snapshot on undefined field undefined foo", + }); }); }); - describe('match', function () { - it('returns a failure when the snapshot does not match', function () { + describe("match", function () { + it("returns a failure when the snapshot does not match", function () { const snapshotManager = new SnapshotManager(); // Ensure this doesn't result in files being written - snapshotManager.willUpdate = 'none'; + snapshotManager.willUpdate = "none"; - const configStub = sinon.stub(snapshotManager, '_getConfig').returns({ - snapshotPath: 'test/__snapshots__/foo.js.snap', - snapshotName: 'testing bar 1' + const configStub = sinon.stub(snapshotManager, "_getConfig").returns({ + snapshotPath: "test/__snapshots__/foo.js.snap", + snapshotName: "testing bar 1", }); const result = snapshotManager.match({}); sinon.assert.calledOnce(configStub); assert.equal(result.pass, false); - assert.equal(typeof result.message, 'function'); + assert.equal(typeof result.message, "function"); assert.match(result.message(), /testing bar 1/); }); - it('match can accept a name hint for failure messages', function () { + it("match can accept a name hint for failure messages", function () { const snapshotManager = new SnapshotManager(); // Ensure this doesn't result in files being written - snapshotManager.willUpdate = 'none'; + snapshotManager.willUpdate = "none"; - const configStub = sinon.stub(snapshotManager, '_getConfig').returns({ - snapshotPath: 'test/__snapshots__/foo.js.snap', - snapshotName: 'testing bar 1' + const configStub = sinon.stub(snapshotManager, "_getConfig").returns({ + snapshotPath: "test/__snapshots__/foo.js.snap", + snapshotName: "testing bar 1", }); - const result = snapshotManager.match({}, {}, '[headers]'); + const result = snapshotManager.match({}, {}, "[headers]"); sinon.assert.calledOnce(configStub); assert.equal(result.pass, false); assert.match(result.message(), /testing bar 1:.*?\[headers\]/); }); - it('executes matcher without properties', function () { + it("executes matcher without properties", function () { const snapshotManager = new SnapshotManager(); // Ensure this doesn't result in files being written - snapshotManager.willUpdate = 'none'; + snapshotManager.willUpdate = "none"; - const configStub = sinon.stub(snapshotManager, '_getConfig').returns({ - snapshotPath: 'test/__snapshots__/foo.js.snap', - snapshotName: 'testing bar 1' + const configStub = sinon.stub(snapshotManager, "_getConfig").returns({ + snapshotPath: "test/__snapshots__/foo.js.snap", + snapshotName: "testing bar 1", }); - const result = snapshotManager.match('foo', null, '[html]'); + const result = snapshotManager.match("foo", null, "[html]"); sinon.assert.calledOnce(configStub); assert.equal(result.pass, false); - assert.equal(typeof result.message, 'function'); + assert.equal(typeof result.message, "function"); }); }); }); diff --git a/packages/jest-snapshot/test/jest-snapshot.test.js b/packages/jest-snapshot/test/jest-snapshot.test.js index ed82abf3f..96c11f1ff 100644 --- a/packages/jest-snapshot/test/jest-snapshot.test.js +++ b/packages/jest-snapshot/test/jest-snapshot.test.js @@ -1,83 +1,108 @@ -const {assert, sinon} = require('./utils'); +const { assert, sinon } = require("./utils"); // We require the root dire -const snapshotTools = require('../'); +const snapshotTools = require("../"); -describe('Jest Snapshot', function () { +describe("Jest Snapshot", function () { afterEach(function () { sinon.restore(); }); - it('exposes a set of functions', function () { - assert.deepEqual(Object.keys(snapshotTools), ['mochaHooks', 'snapshotManager', 'matchSnapshotAssertion', 'any', 'anything', 'stringMatching']); + it("exposes a set of functions", function () { + assert.deepEqual(Object.keys(snapshotTools), [ + "mochaHooks", + "snapshotManager", + "matchSnapshotAssertion", + "any", + "anything", + "stringMatching", + ]); - const {any, anything, stringMatching} = snapshotTools; + const { any, anything, stringMatching } = snapshotTools; // Check the methods we export from other packages still exist and are functions - assert.equal(typeof any, 'function'); - assert.equal(typeof anything, 'function'); - assert.equal(typeof stringMatching, 'function'); + assert.equal(typeof any, "function"); + assert.equal(typeof anything, "function"); + assert.equal(typeof stringMatching, "function"); }); - it('matchSnapshotAssertion calls the match function and asserts the result', function () { - const matchSnapshotSpy = sinon.stub(snapshotTools.snapshotManager, 'match').returns( - { - message: () => { }, - pass: { - should: { - eql: () => { } - } - } - } - ); - const fakeThis = {obj: {foo: 'bar'}, assert: () => {}}; + it("matchSnapshotAssertion calls the match function and asserts the result", function () { + const matchSnapshotSpy = sinon.stub(snapshotTools.snapshotManager, "match").returns({ + message: () => {}, + pass: { + should: { + eql: () => {}, + }, + }, + }); + const fakeThis = { obj: { foo: "bar" }, assert: () => {} }; const fakeProps = {}; snapshotTools.matchSnapshotAssertion.call(fakeThis, fakeProps); sinon.assert.calledOnce(matchSnapshotSpy); sinon.assert.calledOnceWithExactly(matchSnapshotSpy, fakeThis.obj, fakeProps); }); - describe('mochaHooks', function () { - it('beforeAll correctly resets the registry', function () { - const registrySpy = sinon.stub(snapshotTools.snapshotManager, 'resetRegistry'); + describe("mochaHooks", function () { + it("beforeAll correctly resets the registry", function () { + const registrySpy = sinon.stub(snapshotTools.snapshotManager, "resetRegistry"); snapshotTools.mochaHooks.beforeAll(); sinon.assert.calledOnce(registrySpy); }); - it('beforeEach correctly sets the current test', function () { - const setTestSpy = sinon.stub(snapshotTools.snapshotManager, 'setCurrentTest').returns(); - snapshotTools.mochaHooks.beforeEach.call({currentTest: {file: 'test', fullTitle: () => { }, currentRetry: () => 0}}); + it("beforeEach correctly sets the current test", function () { + const setTestSpy = sinon + .stub(snapshotTools.snapshotManager, "setCurrentTest") + .returns(); + snapshotTools.mochaHooks.beforeEach.call({ + currentTest: { file: "test", fullTitle: () => {}, currentRetry: () => 0 }, + }); sinon.assert.calledOnce(setTestSpy); }); - it('beforeEach with retries correctly resets the registry for the current test', function () { - const setTestSpy = sinon.stub(snapshotTools.snapshotManager, 'setCurrentTest').returns(); - const resetRegistrySpy = sinon.stub(snapshotTools.snapshotManager, 'resetRegistryForCurrentTest').returns(); - snapshotTools.mochaHooks.beforeEach.call({currentTest: {file: 'test', fullTitle: () => { }, currentRetry: () => 0}}); - snapshotTools.mochaHooks.beforeEach.call({currentTest: {file: 'test', fullTitle: () => { }, currentRetry: () => 1}}); + it("beforeEach with retries correctly resets the registry for the current test", function () { + const setTestSpy = sinon + .stub(snapshotTools.snapshotManager, "setCurrentTest") + .returns(); + const resetRegistrySpy = sinon + .stub(snapshotTools.snapshotManager, "resetRegistryForCurrentTest") + .returns(); + snapshotTools.mochaHooks.beforeEach.call({ + currentTest: { file: "test", fullTitle: () => {}, currentRetry: () => 0 }, + }); + snapshotTools.mochaHooks.beforeEach.call({ + currentTest: { file: "test", fullTitle: () => {}, currentRetry: () => 1 }, + }); sinon.assert.calledTwice(setTestSpy); sinon.assert.calledOnce(resetRegistrySpy); }); }); - describe('framework expectations', function () { + describe("framework expectations", function () { // The expectation of this framework is that you wire up the beforeAll and beforeEach hooks // and then call snapshotManager.match in your tests, either directly, via matchSnapshotAssertion // or via the snapshotManager.assertSnapshot method. // This test checks that match works as expected when using beforeAll and beforeEach - const testFile = 'test/framework-expectations-test.js'; - const firstTestTitle = 'My first test'; - const secondTestTitle = 'My second test'; + const testFile = "test/framework-expectations-test.js"; + const firstTestTitle = "My first test"; + const secondTestTitle = "My second test"; - it('snapshot matching works as expected when using beforeAll and beforeEach', function () { + it("snapshot matching works as expected when using beforeAll and beforeEach", function () { // Setup some spies - const resetRegistrySpy = sinon.spy(snapshotTools.snapshotManager, 'resetRegistry'); - const setCurrentTestSpy = sinon.spy(snapshotTools.snapshotManager, 'setCurrentTest'); - - const firstTest = {file: `${testFile}`, fullTitle: () => firstTestTitle, currentRetry: () => 0}; - const secondTest = {file: `${testFile}`, fullTitle: () => secondTestTitle, currentRetry: () => 0}; - const expectedResult = {foo: 'bar'}; + const resetRegistrySpy = sinon.spy(snapshotTools.snapshotManager, "resetRegistry"); + const setCurrentTestSpy = sinon.spy(snapshotTools.snapshotManager, "setCurrentTest"); + + const firstTest = { + file: `${testFile}`, + fullTitle: () => firstTestTitle, + currentRetry: () => 0, + }; + const secondTest = { + file: `${testFile}`, + fullTitle: () => secondTestTitle, + currentRetry: () => 0, + }; + const expectedResult = { foo: "bar" }; let testResult; @@ -87,51 +112,60 @@ describe('Jest Snapshot', function () { assert.deepEqual(snapshotTools.snapshotManager.registry, {}); // Execute a test calling beforeEach and using match - snapshotTools.mochaHooks.beforeEach.call({currentTest: firstTest}); + snapshotTools.mochaHooks.beforeEach.call({ currentTest: firstTest }); testResult = snapshotTools.snapshotManager.match(expectedResult); // Check the test was called how we expected sinon.assert.calledOnce(setCurrentTestSpy); - assert.deepEqual(setCurrentTestSpy.firstCall.args[0], {testPath: `${testFile}`, testTitle: firstTestTitle}); + assert.deepEqual(setCurrentTestSpy.firstCall.args[0], { + testPath: `${testFile}`, + testTitle: firstTestTitle, + }); assert.equal(testResult.pass, true); // Check the registry looks as expected assert.deepEqual(snapshotTools.snapshotManager.registry, { [`${testFile}`]: { - [`${firstTestTitle}`]: 1 - } + [`${firstTestTitle}`]: 1, + }, }); // Execute a second test - snapshotTools.mochaHooks.beforeEach.call({currentTest: secondTest}); + snapshotTools.mochaHooks.beforeEach.call({ currentTest: secondTest }); testResult = snapshotTools.snapshotManager.match(expectedResult); sinon.assert.calledTwice(setCurrentTestSpy); - assert.deepEqual(setCurrentTestSpy.secondCall.args[0], {testPath: `${testFile}`, testTitle: secondTestTitle}); + assert.deepEqual(setCurrentTestSpy.secondCall.args[0], { + testPath: `${testFile}`, + testTitle: secondTestTitle, + }); assert.equal(testResult.pass, false); // Check the registry looks as expected assert.deepEqual(snapshotTools.snapshotManager.registry, { [`${testFile}`]: { [`${firstTestTitle}`]: 1, - [`${secondTestTitle}`]: 1 - } + [`${secondTestTitle}`]: 1, + }, }); // Execute a third test, which is the second test duplicated - snapshotTools.mochaHooks.beforeEach.call({currentTest: secondTest}); + snapshotTools.mochaHooks.beforeEach.call({ currentTest: secondTest }); testResult = snapshotTools.snapshotManager.match(expectedResult); sinon.assert.calledThrice(setCurrentTestSpy); - assert.deepEqual(setCurrentTestSpy.thirdCall.args[0], {testPath: `${testFile}`, testTitle: secondTestTitle}); + assert.deepEqual(setCurrentTestSpy.thirdCall.args[0], { + testPath: `${testFile}`, + testTitle: secondTestTitle, + }); assert.equal(testResult.pass, false); // Check the registry looks as expected assert.deepEqual(snapshotTools.snapshotManager.registry, { [`${testFile}`]: { [`${firstTestTitle}`]: 1, - [`${secondTestTitle}`]: 2 - } + [`${secondTestTitle}`]: 2, + }, }); }); }); diff --git a/packages/jest-snapshot/test/utils.test.js b/packages/jest-snapshot/test/utils.test.js index aaf215785..50cba7f5d 100644 --- a/packages/jest-snapshot/test/utils.test.js +++ b/packages/jest-snapshot/test/utils.test.js @@ -1,11 +1,14 @@ -const {assert} = require('./utils'); +const { assert } = require("./utils"); -const {makeMessageFromMatchMessage} = require('../lib/utils'); +const { makeMessageFromMatchMessage } = require("../lib/utils"); -describe('Utils', function () { - describe('makeMessageFromMatchMessage', function () { - it('makeMessageFromMatchMessage', function () { - assert.equal(makeMessageFromMatchMessage('remove me\nnice test', 'substitute'), 'substitute\nnice test'); +describe("Utils", function () { + describe("makeMessageFromMatchMessage", function () { + it("makeMessageFromMatchMessage", function () { + assert.equal( + makeMessageFromMatchMessage("remove me\nnice test", "substitute"), + "substitute\nnice test", + ); }); }); }); diff --git a/packages/jest-snapshot/test/utils/index.js b/packages/jest-snapshot/test/utils/index.js index 2d86c0b41..55a097df4 100644 --- a/packages/jest-snapshot/test/utils/index.js +++ b/packages/jest-snapshot/test/utils/index.js @@ -4,10 +4,10 @@ * Shared utils for writing tests */ -const sinon = require('sinon'); +const sinon = require("sinon"); // Require overrides - these add globals for tests module.exports = { sinon, - assert: require('assert') + assert: require("assert"), }; diff --git a/packages/job-manager/README.md b/packages/job-manager/README.md index b23248572..56d60e7fb 100644 --- a/packages/job-manager/README.md +++ b/packages/job-manager/README.md @@ -7,6 +7,7 @@ A manager for background jobs in Ghost, supporting one-off tasks and recurring j Background job orchestration for Ghost, supporting one-off and recurring jobs in-process or offloaded workers. ## Table of Contents + - [Quick Start](#quick-start) - [Job Types](#job-types) - [Background Job Requirements](#background-job-requirements) @@ -16,21 +17,21 @@ Background job orchestration for Ghost, supporting one-off and recurring jobs in ## Quick Start ```js -const JobManager = require('@tryghost/job-manager'); -const jobManager = new JobManager({JobModel: models.Job}); +const JobManager = require("@tryghost/job-manager"); +const jobManager = new JobManager({ JobModel: models.Job }); // Simple one-off job jobManager.addJob({ - name: 'hello-world', - job: () => console.log('Hello World'), - offloaded: false + name: "hello-world", + job: () => console.log("Hello World"), + offloaded: false, }); // Recurring job every 5 minutes jobManager.addJob({ - name: 'check-emails', - at: 'every 5 minutes', - job: './jobs/check-emails.js' + name: "check-emails", + at: "every 5 minutes", + job: "./jobs/check-emails.js", }); ``` @@ -39,14 +40,14 @@ jobManager.addJob({ Ghost supports two types of jobs: 1. **Inline Jobs** - - Run in the main Ghost process - - Best for quick, simple tasks - - Cannot be scheduled or recurring + - Run in the main Ghost process + - Best for quick, simple tasks + - Cannot be scheduled or recurring 2. **Offloaded Jobs** - - Run in a separate process - - Good for CPU-intensive tasks - - Can be scheduled or recurring + - Run in a separate process + - Good for CPU-intensive tasks + - Can be scheduled or recurring ## Background Job Requirements @@ -61,75 +62,76 @@ Offloaded jobs must: ## Advanced Usage Below is a sample code to wire up job manger and initialize jobs. This is the simplest way to interact with the job manager - these jobs do not persist after reboot: + ```js -const JobManager = require('@tryghost/job-manager'); +const JobManager = require("@tryghost/job-manager"); -const jobManager = new JobManager({JobModel: models.Job}); +const jobManager = new JobManager({ JobModel: models.Job }); // register a job "function" with queued execution in current event loop jobManager.addJob({ job: (word) => console.log(word), - name: 'hello', - offloaded: false + name: "hello", + offloaded: false, }); // register a job "module" with queued execution in current even loop jobManager.addJob({ - job:'./path/to/email-module.js', - data: {email: 'send@here.com'}, - offloaded: false + job: "./path/to/email-module.js", + data: { email: "send@here.com" }, + offloaded: false, }); // register recurring job which needs execution outside of current event loop jobManager.addJob({ - at: 'every 5 minutes', - job: './path/to/jobs/check-emails.js', - name: 'email-checker' + at: "every 5 minutes", + job: "./path/to/jobs/check-emails.js", + name: "email-checker", }); // register recurring job with cron syntax running every 5 minutes // job needs execution outside of current event loop // for cron builder check https://crontab.guru/ (first value is seconds) jobManager.addJob({ - at: '0 1/5 * * * *', - job: './path/to/jobs/check-emails.js', - name: 'email-checker-cron' + at: "0 1/5 * * * *", + job: "./path/to/jobs/check-emails.js", + name: "email-checker-cron", }); // register a job to run immediately running outside of current even loop jobManager.addJob({ - job: './path/to/jobs/check-emails.js', - name: 'email-checker-now' + job: "./path/to/jobs/check-emails.js", + name: "email-checker-now", }); // register a one-off job to be executed immediately within current event loop jobManager.addOneOffJob({ - name: 'members-migrations', + name: "members-migrations", offloaded: false, - job: stripeService.migrations.execute.bind(stripeService.migrations) + job: stripeService.migrations.execute.bind(stripeService.migrations), }); // register a one-off job to be executed immediately outside of current event loop jobManager.addOneOffJob({ - name: 'generate-backup-2022-09-15', - job: './path/to/jobs/backup.js', + name: "generate-backup-2022-09-15", + job: "./path/to/jobs/backup.js", }); -// optionally await completion of the one-off job in case +// optionally await completion of the one-off job in case // there are state changes expected to execute the rest of the process // NOTE: if multiple jobs are submitted using the same name, the first completion will resolve -await jobManager.awaitOneOffCompletion('members-migrations'); +await jobManager.awaitOneOffCompletion("members-migrations"); -// check if previously registered one-off job has been executed +// check if previously registered one-off job has been executed // successfully - it exists and doesn't have a "failed" state. // NOTE: this is stored in memory and cleared on reboot -const backupSuccessful = await jobManager.hasExecutedSuccessfully('generate-backup-2022-09-15'); +const backupSuccessful = await jobManager.hasExecutedSuccessfully("generate-backup-2022-09-15"); if (!backupSuccessful) { // One-off jobs with "failed" state can be rescheduled jobManager.addOneOffJob({ - name: 'generate-backup-2022-09-15', - job: './path/to/jobs/backup.js', + name: "generate-backup-2022-09-15", + job: "./path/to/jobs/backup.js", }); } ``` @@ -139,16 +141,20 @@ For more examples of JobManager initialization check [test/examples](https://git ## Technical Details ### Process Isolation + Background jobs run in separate processes, meaning: + - No shared memory with Ghost - Must import own dependencies - Should avoid heavy dependencies - Should handle graceful shutdown ### Job Lifecycle + Offloaded jobs are running on dedicated worker threads which makes their lifecycle a bit different from inline jobs: + 1. When **starting** a job it's only sharing ENV variables with its parent process. The job itself is run on an independent JavaScript execution thread. The script has to re-initialize any modules it will use. For example, it should take care of: model layer initialization, cache initialization, etc. -2. When **finishing** work in a job prefer to signal successful termination by sending 'done' message to the parent thread: `parentPort.postMessage('done')` ([example use](https://github.com/TryGhost/Utils/blob/0e423f6c5c69b08d81d470f49de95654d8cc90e3/packages/job-manager/test/jobs/graceful.js#L33-L37)). Finishing work this way terminates the thread through [worker.terminate()]((https://nodejs.org/dist/latest-v14.x/docs/api/worker_threads.html#worker_threads_worker_terminate)), which logs termination in parent process and flushes any pipes opened in thread. +2. When **finishing** work in a job prefer to signal successful termination by sending 'done' message to the parent thread: `parentPort.postMessage('done')` ([example use](https://github.com/TryGhost/Utils/blob/0e423f6c5c69b08d81d470f49de95654d8cc90e3/packages/job-manager/test/jobs/graceful.js#L33-L37)). Finishing work this way terminates the thread through [worker.terminate()](<(https://nodejs.org/dist/latest-v14.x/docs/api/worker_threads.html#worker_threads_worker_terminate)>), which logs termination in parent process and flushes any pipes opened in thread. 3. Jobs that have iterative nature, or need cleanup before interrupting work should allow for **graceful shutdown** by listening on `'cancel'` message coming from parent thread ([example use](https://github.com/TryGhost/Utils/blob/0e423f6c5c69b08d81d470f49de95654d8cc90e3/packages/job-manager/test/jobs/graceful.js#L12-L16)). 4. When **exceptions** happen and expected outcome is to terminate current job, leave the exception unhandled allowing it to bubble up to the job manager. Unhandled exceptions [terminate current thread](https://nodejs.org/dist/latest-v14.x/docs/api/worker_threads.html#worker_threads_event_error) and allow for next scheduled job execution to happen. diff --git a/packages/job-manager/index.js b/packages/job-manager/index.js index 862a24c9c..077b4f604 100644 --- a/packages/job-manager/index.js +++ b/packages/job-manager/index.js @@ -1 +1 @@ -module.exports = require('./lib/JobManager'); +module.exports = require("./lib/JobManager"); diff --git a/packages/job-manager/lib/JobManager.js b/packages/job-manager/lib/JobManager.js index 988e4017d..a485ef81d 100644 --- a/packages/job-manager/lib/JobManager.js +++ b/packages/job-manager/lib/JobManager.js @@ -1,14 +1,14 @@ -const path = require('path'); -const util = require('util'); +const path = require("path"); +const util = require("util"); const setTimeoutPromise = util.promisify(setTimeout); -const fastq = require('fastq'); -const later = require('@breejs/later'); -const Bree = require('bree'); -const {UnhandledJobError, IncorrectUsageError} = require('@tryghost/errors'); -const logging = require('@tryghost/logging'); -const isCronExpression = require('./is-cron-expression'); -const assembleBreeJob = require('./assemble-bree-job'); -const JobsRepository = require('./JobsRepository'); +const fastq = require("fastq"); +const later = require("@breejs/later"); +const Bree = require("bree"); +const { UnhandledJobError, IncorrectUsageError } = require("@tryghost/errors"); +const logging = require("@tryghost/logging"); +const isCronExpression = require("./is-cron-expression"); +const assembleBreeJob = require("./assemble-bree-job"); +const JobsRepository = require("./JobsRepository"); const worker = async (task, callback) => { try { @@ -20,10 +20,10 @@ const worker = async (task, callback) => { }; const ALL_STATUSES = { - started: 'started', - finished: 'finished', - failed: 'failed', - queued: 'queued' + started: "started", + finished: "finished", + failed: "failed", + queued: "queued", }; /** @@ -50,7 +50,14 @@ class JobManager { * @param {Object} [options.config] - config * @param {Object} [options.events] - events instance (for testing) */ - constructor({errorHandler, workerMessageHandler, JobModel, domainEvents, config, events = null}) { + constructor({ + errorHandler, + workerMessageHandler, + JobModel, + domainEvents, + config, + events = null, + }) { this.inlineQueue = fastq(this, worker, 3); this._jobMessageHandler = this._jobMessageHandler.bind(this); this._jobErrorHandler = this._jobErrorHandler.bind(this); @@ -60,17 +67,17 @@ class JobManager { this.#events = events; const combinedMessageHandler = workerMessageHandler - ? ({name, message}) => { - workerMessageHandler({name, message}); - this._jobMessageHandler({name, message}); - } + ? ({ name, message }) => { + workerMessageHandler({ name, message }); + this._jobMessageHandler({ name, message }); + } : this._jobMessageHandler; const combinedErrorHandler = errorHandler ? (error, workerMeta) => { - errorHandler(error, workerMeta); - this._jobErrorHandler(error, workerMeta); - } + errorHandler(error, workerMeta); + this._jobErrorHandler(error, workerMeta); + } : this._jobErrorHandler; this.bree = new Bree({ @@ -79,15 +86,15 @@ class JobManager { outputWorkerMetadata: true, logger: logging, errorHandler: combinedErrorHandler, - workerMessageHandler: combinedMessageHandler + workerMessageHandler: combinedMessageHandler, }); - this.bree.on('worker created', (name) => { - this._jobMessageHandler({name, message: ALL_STATUSES.started}); + this.bree.on("worker created", (name) => { + this._jobMessageHandler({ name, message: ALL_STATUSES.started }); }); if (JobModel) { - this._jobsRepository = new JobsRepository({JobModel}); + this._jobsRepository = new JobsRepository({ JobModel }); } } @@ -95,12 +102,12 @@ class JobManager { return async (error, result) => { if (error) { await this._jobErrorHandler(error, { - name: jobName + name: jobName, }); } else { await this._jobMessageHandler({ name: jobName, - message: 'done' + message: "done", }); } @@ -109,7 +116,7 @@ class JobManager { }; } - async _jobMessageHandler({name, message}) { + async _jobMessageHandler({ name, message }) { if (name) { if (message === ALL_STATUSES.started) { if (this._jobsRepository) { @@ -118,18 +125,18 @@ class JobManager { if (job) { await this._jobsRepository.update(job.id, { status: ALL_STATUSES.started, - started_at: new Date() + started_at: new Date(), }); } } - } else if (message === 'done') { + } else if (message === "done") { if (this._jobsRepository) { const job = await this._jobsRepository.read(name); if (job) { await this._jobsRepository.update(job.id, { status: ALL_STATUSES.finished, - finished_at: new Date() + finished_at: new Date(), }); } } @@ -144,16 +151,16 @@ class JobManager { } if (this.inlineQueue.length() <= 1) { - if (this.#completionPromises.has('all')) { - for (const listeners of this.#completionPromises.get('all')) { + if (this.#completionPromises.has("all")) { + for (const listeners of this.#completionPromises.get("all")) { listeners.resolve(); } // Clear the listeners - this.#completionPromises.delete('all'); + this.#completionPromises.delete("all"); } } } else { - if (typeof message === 'object' && this.#domainEvents) { + if (typeof message === "object" && this.#domainEvents) { // Is this an event? if (message.event) { this.#domainEvents.dispatchRaw(message.event.type, message.event.data); @@ -169,7 +176,7 @@ class JobManager { if (job) { await this._jobsRepository.update(job.id, { - status: ALL_STATUSES.failed + status: ALL_STATUSES.failed, }); } } @@ -184,12 +191,12 @@ class JobManager { } if (this.inlineQueue.length() <= 1) { - if (this.#completionPromises.has('all')) { - for (const listeners of this.#completionPromises.get('all')) { + if (this.#completionPromises.has("all")) { + for (const listeners of this.#completionPromises.get("all")) { listeners.reject(error); } // Clear the listeners - this.#completionPromises.delete('all'); + this.#completionPromises.delete("all"); } } } @@ -205,17 +212,17 @@ class JobManager { * @prop {Object} [GhostJob.data] - data to be passed into the job * @prop {Boolean} [GhostJob.offloaded] - creates an "offloaded" job running in a worker thread by default. If set to "false" runs an "inline" job on the same event loop */ - async addJob({name, at, job, data, offloaded = true}) { + async addJob({ name, at, job, data, offloaded = true }) { if (offloaded) { - logging.info('Adding offloaded job to the inline job queue'); + logging.info("Adding offloaded job to the inline job queue"); let schedule; if (!name) { - if (typeof job === 'string') { + if (typeof job === "string") { name = path.parse(job).name; } else { throw new IncorrectUsageError({ - message: 'Name parameter should be present if job is a function' + message: "Name parameter should be present if job is a function", }); } } @@ -229,11 +236,13 @@ class JobManager { if ((schedule.error && schedule.error !== -1) || schedule.schedules.length === 0) { throw new IncorrectUsageError({ - message: 'Invalid schedule format' + message: "Invalid schedule format", }); } - logging.info(`Scheduling job ${name} at ${at}. Next run on: ${later.schedule(schedule).next()}`); + logging.info( + `Scheduling job ${name} at ${at}. Next run on: ${later.schedule(schedule).next()}`, + ); } else if (at !== undefined) { logging.info(`Scheduling job ${name} at ${at}`); } else { @@ -244,7 +253,9 @@ class JobManager { await this.bree.add(breeJob); return this.bree.start(name); } else { - logging.info(`Adding one-off job to inlineQueue with current length = ${this.inlineQueue.length()} called '${name || 'anonymous'}'`); + logging.info( + `Adding one-off job to inlineQueue with current length = ${this.inlineQueue.length()} called '${name || "anonymous"}'`, + ); this.inlineQueue.push(async () => { try { @@ -252,10 +263,10 @@ class JobManager { // distinguish between states when the job fails immediately await this._jobMessageHandler({ name: name, - message: ALL_STATUSES.started + message: ALL_STATUSES.started, }); - if (typeof job === 'function') { + if (typeof job === "function") { await job(data); } else { await require(job)(data); @@ -263,10 +274,12 @@ class JobManager { } catch (err) { // NOTE: each job should be written in a safe way and handle all errors internally // if the error is caught here jobs implementation should be changed - logging.error(new UnhandledJobError({ - context: (typeof job === 'function') ? 'function' : job, - err - })); + logging.error( + new UnhandledJobError({ + context: typeof job === "function" ? "function" : job, + err, + }), + ); throw err; } @@ -275,39 +288,39 @@ class JobManager { } /** - * Adds a job that could ever be executed once. In case the job fails - * can be "added" again, effectively restarting the failed job. - * - * @param {Object} GhostJob - job options - * @prop {Function | String} GhostJob.job - function or path to a module defining a job - * @prop {String} GhostJob.name - unique job name, if not provided takes function name or job script filename - * @prop {String | Date} [GhostJob.at] - Date, cron or human readable schedule format. Manage will do immediate execution if not specified. Not supported for "inline" jobs - * @prop {Object} [GhostJob.data] - data to be passed into the job - * @prop {Boolean} [GhostJob.offloaded] - creates an "offloaded" job running in a worker thread by default. If set to "false" runs an "inline" job on the same event loop - */ - async addOneOffJob({name, job, data, offloaded = true}) { + * Adds a job that could ever be executed once. In case the job fails + * can be "added" again, effectively restarting the failed job. + * + * @param {Object} GhostJob - job options + * @prop {Function | String} GhostJob.job - function or path to a module defining a job + * @prop {String} GhostJob.name - unique job name, if not provided takes function name or job script filename + * @prop {String | Date} [GhostJob.at] - Date, cron or human readable schedule format. Manage will do immediate execution if not specified. Not supported for "inline" jobs + * @prop {Object} [GhostJob.data] - data to be passed into the job + * @prop {Boolean} [GhostJob.offloaded] - creates an "offloaded" job running in a worker thread by default. If set to "false" runs an "inline" job on the same event loop + */ + async addOneOffJob({ name, job, data, offloaded = true }) { if (!name) { throw new IncorrectUsageError({ - message: `The name parameter is required for a one off job.` + message: `The name parameter is required for a one off job.`, }); } const persistedJob = await this._jobsRepository.read(name); - if (persistedJob && (persistedJob.get('status') !== ALL_STATUSES.failed)) { + if (persistedJob && persistedJob.get("status") !== ALL_STATUSES.failed) { throw new IncorrectUsageError({ - message: `A "${name}" one off job has already been executed.` + message: `A "${name}" one off job has already been executed.`, }); } - if (persistedJob && (persistedJob.get('status') === ALL_STATUSES.failed)) { + if (persistedJob && persistedJob.get("status") === ALL_STATUSES.failed) { await this._jobsRepository.update(persistedJob.id, { - status: ALL_STATUSES.queued + status: ALL_STATUSES.queued, }); } else { await this._jobsRepository.add({ name, - status: ALL_STATUSES.queued + status: ALL_STATUSES.queued, }); } @@ -316,7 +329,7 @@ class JobManager { // For example, it failed and the process was restarted. // If we want to be able to restart within the same instance, // we'd need to handle job restart/removal in Bree first - this.addJob({name, job, data, offloaded}); + this.addJob({ name, job, data, offloaded }); } /** @@ -330,7 +343,7 @@ class JobManager { if (!persistedJob) { return false; } else { - return (persistedJob.get('status') !== ALL_STATUSES.failed); + return persistedJob.get("status") !== ALL_STATUSES.failed; } } else { return false; @@ -346,7 +359,10 @@ class JobManager { async awaitOneOffCompletion(name) { const persistedJob = await this._jobsRepository.read(name); - if (!persistedJob || ![ALL_STATUSES.finished, ALL_STATUSES.failed].includes(persistedJob.get('status'))) { + if ( + !persistedJob || + ![ALL_STATUSES.finished, ALL_STATUSES.failed].includes(persistedJob.get("status")) + ) { // NOTE: can implement exponential backoff here if that's ever needed await setTimeoutPromise(500); @@ -365,7 +381,7 @@ class JobManager { const promise = new Promise((resolve, reject) => { this.#completionPromises.set(name, [ ...(this.#completionPromises.get(name) ?? []), - {resolve, reject} + { resolve, reject }, ]); }); @@ -376,7 +392,7 @@ class JobManager { * Wait for all inline jobs to be completed. */ async allSettled() { - const name = 'all'; + const name = "all"; return new Promise((resolve, reject) => { if (this.inlineQueue.idle()) { @@ -386,7 +402,7 @@ class JobManager { this.#completionPromises.set(name, [ ...(this.#completionPromises.get(name) ?? []), - {resolve, reject} + { resolve, reject }, ]); }); } @@ -415,12 +431,12 @@ class JobManager { return; } - logging.warn('Waiting for busy job in inline job queue'); + logging.warn("Waiting for busy job in inline job queue"); - const {default: pWaitFor} = await import('p-wait-for'); + const { default: pWaitFor } = await import("p-wait-for"); await pWaitFor(() => this.inlineQueue.idle() === true, options); - logging.warn('Inline job queue finished'); + logging.warn("Inline job queue finished"); } } diff --git a/packages/job-manager/lib/JobsRepository.js b/packages/job-manager/lib/JobsRepository.js index 2646a8c80..bc285b80a 100644 --- a/packages/job-manager/lib/JobsRepository.js +++ b/packages/job-manager/lib/JobsRepository.js @@ -1,4 +1,4 @@ -const logging = require('@tryghost/logging'); +const logging = require("@tryghost/logging"); /** * @class JobsRepository @@ -10,7 +10,7 @@ class JobsRepository { * @param {Object} options - The options object. * @param {Object} options.JobModel - The Job model for database operations. */ - constructor({JobModel}) { + constructor({ JobModel }) { // NOTE: We ought to clean this up. We want to use bookshelf models for all db operations, // but we use knex directly in a few places still largely for performance reasons. this._JobModel = JobModel; @@ -36,7 +36,7 @@ class JobsRepository { * @returns {Promise} The job object if found, null otherwise. */ async read(name) { - const job = await this._JobModel.findOne({name}); + const job = await this._JobModel.findOne({ name }); return job; } @@ -49,7 +49,7 @@ class JobsRepository { * @returns {Promise} */ async update(id, data) { - await this._JobModel.edit(data, {id}); + await this._JobModel.edit(data, { id }); } /** @@ -61,11 +61,11 @@ class JobsRepository { */ async delete(id) { try { - await this._JobModel.destroy({id}); + await this._JobModel.destroy({ id }); } catch (error) { logging.error(`Error deleting job ${id}:`, error); } } } -module.exports = JobsRepository; \ No newline at end of file +module.exports = JobsRepository; diff --git a/packages/job-manager/lib/assemble-bree-job.js b/packages/job-manager/lib/assemble-bree-job.js index 64792db5c..e60282fb7 100644 --- a/packages/job-manager/lib/assemble-bree-job.js +++ b/packages/job-manager/lib/assemble-bree-job.js @@ -1,4 +1,4 @@ -const isCronExpression = require('./is-cron-expression'); +const isCronExpression = require("./is-cron-expression"); /** * Creates job Object compatible with bree job definition (https://github.com/breejs/bree#job-options) @@ -12,28 +12,28 @@ const assemble = (at, job, data, name) => { const breeJob = { name: name, // NOTE: both function and path syntaxes work with 'path' parameter - path: job + path: job, }; if (data) { Object.assign(breeJob, { worker: { - workerData: data - } + workerData: data, + }, }); } if (at instanceof Date) { Object.assign(breeJob, { - date: at + date: at, }); } else if (at && isCronExpression(at)) { Object.assign(breeJob, { - cron: at + cron: at, }); } else if (at !== undefined) { Object.assign(breeJob, { - interval: at + interval: at, }); } diff --git a/packages/job-manager/lib/is-cron-expression.js b/packages/job-manager/lib/is-cron-expression.js index 02dbf28f0..2a5bdef2c 100644 --- a/packages/job-manager/lib/is-cron-expression.js +++ b/packages/job-manager/lib/is-cron-expression.js @@ -1,4 +1,4 @@ -const cronValidateModule = require('cron-validate'); +const cronValidateModule = require("cron-validate"); /* c8 ignore next 1 */ const cronValidate = cronValidateModule.default || cronValidateModule; @@ -26,10 +26,10 @@ const cronValidate = cronValidateModule.default || cronValidateModule; */ const isCronExpression = (expression) => { let cronResult = cronValidate(expression, { - preset: 'default', // second field not supported in default preset + preset: "default", // second field not supported in default preset override: { - useSeconds: true // override preset option - } + useSeconds: true, // override preset option + }, }); return cronResult.isValid(); diff --git a/packages/job-manager/package.json b/packages/job-manager/package.json index 6ba548616..9e5e2a7f0 100644 --- a/packages/job-manager/package.json +++ b/packages/job-manager/package.json @@ -1,12 +1,21 @@ { "name": "@tryghost/job-manager", "version": "3.0.3", - "author": "Ghost Foundation", "license": "MIT", + "author": "Ghost Foundation", + "repository": { + "type": "git", + "url": "git+https://github.com/TryGhost/framework.git", + "directory": "packages/job-manager" + }, + "files": [ + "index.js", + "lib" + ], + "main": "index.js", "publishConfig": { "access": "public" }, - "main": "index.js", "scripts": { "dev": "echo \"Implement me!\"", "test:unit": "NODE_ENV=testing vitest run --coverage", @@ -16,16 +25,6 @@ "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", "lint:eslint": "yarn lint:code && yarn lint:test" }, - "files": [ - "index.js", - "lib" - ], - "devDependencies": { - "@sinonjs/fake-timers": "15.1.1", - "date-fns": "4.1.0", - "rewire": "9.0.1", - "sinon": "21.0.3" - }, "dependencies": { "@breejs/later": "4.2.0", "@tryghost/errors": "^3.0.3", @@ -36,9 +35,10 @@ "p-wait-for": "6.0.0", "workerpool": "10.0.1" }, - "repository": { - "type": "git", - "url": "git+https://github.com/TryGhost/framework.git", - "directory": "packages/job-manager" + "devDependencies": { + "@sinonjs/fake-timers": "15.1.1", + "date-fns": "4.1.0", + "rewire": "9.0.1", + "sinon": "21.0.3" } } diff --git a/packages/job-manager/test/.eslintrc.js b/packages/job-manager/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/job-manager/test/.eslintrc.js +++ b/packages/job-manager/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/job-manager/test/examples/graceful-shutdown.js b/packages/job-manager/test/examples/graceful-shutdown.js index dd479cae9..8ae1f7272 100644 --- a/packages/job-manager/test/examples/graceful-shutdown.js +++ b/packages/job-manager/test/examples/graceful-shutdown.js @@ -1,17 +1,17 @@ /* eslint-disable no-console */ -const path = require('path'); -const setTimeoutPromise = require('util').promisify(setTimeout); -const JobManager = require('../../lib/job-manager'); +const path = require("path"); +const setTimeoutPromise = require("util").promisify(setTimeout); +const JobManager = require("../../lib/job-manager"); const jobManager = new JobManager({ info: console.log, warn: console.log, - error: console.log + error: console.log, }); -process.on('SIGINT', () => { - shutdown('SIGINT'); +process.on("SIGINT", () => { + shutdown("SIGINT"); }); async function shutdown(signal) { @@ -22,14 +22,18 @@ async function shutdown(signal) { (async () => { jobManager.addJob({ - at: 'every 10 seconds', - job: path.resolve(__dirname, '../jobs/graceful.js') + at: "every 10 seconds", + job: path.resolve(__dirname, "../jobs/graceful.js"), }); await setTimeoutPromise(100); // allow job to get scheduled - const {default: pWaitFor} = await import('p-wait-for'); - await pWaitFor(() => (Object.keys(jobManager.bree.workers).length === 0) && (Object.keys(jobManager.bree.intervals).length === 0)); + const { default: pWaitFor } = await import("p-wait-for"); + await pWaitFor( + () => + Object.keys(jobManager.bree.workers).length === 0 && + Object.keys(jobManager.bree.intervals).length === 0, + ); process.exit(0); })(); diff --git a/packages/job-manager/test/examples/scheduled-one-off.js b/packages/job-manager/test/examples/scheduled-one-off.js index ae10fac15..4ac5bc6b0 100644 --- a/packages/job-manager/test/examples/scheduled-one-off.js +++ b/packages/job-manager/test/examples/scheduled-one-off.js @@ -1,13 +1,15 @@ -const path = require('path'); -const addSeconds = require('date-fns/addSeconds'); -const JobManager = require('../../lib/job-manager'); +const path = require("path"); +const addSeconds = require("date-fns/addSeconds"); +const JobManager = require("../../lib/job-manager"); const jobManager = new JobManager(console); const isJobQueueEmpty = (bree) => { - return (Object.keys(bree.workers).length === 0) - && (Object.keys(bree.intervals).length === 0) - && (Object.keys(bree.timeouts).length === 0); + return ( + Object.keys(bree.workers).length === 0 && + Object.keys(bree.intervals).length === 0 && + Object.keys(bree.timeouts).length === 0 + ); }; (async () => { @@ -15,15 +17,15 @@ const isJobQueueEmpty = (bree) => { jobManager.addJob({ at: dateInTenSeconds, - job: path.resolve(__dirname, '../jobs/timed-job.js'), + job: path.resolve(__dirname, "../jobs/timed-job.js"), data: { - ms: 2000 + ms: 2000, }, - name: 'one-off-scheduled-job' + name: "one-off-scheduled-job", }); - const {default: pWaitFor} = await import('p-wait-for'); - await pWaitFor(() => (isJobQueueEmpty(jobManager.bree))); + const { default: pWaitFor } = await import("p-wait-for"); + await pWaitFor(() => isJobQueueEmpty(jobManager.bree)); process.exit(0); })(); diff --git a/packages/job-manager/test/is-cron-expression.test.js b/packages/job-manager/test/is-cron-expression.test.js index 49c8880fe..4f1dc1e25 100644 --- a/packages/job-manager/test/is-cron-expression.test.js +++ b/packages/job-manager/test/is-cron-expression.test.js @@ -1,43 +1,43 @@ -const assert = require('assert/strict'); -const rewire = require('rewire'); +const assert = require("assert/strict"); +const rewire = require("rewire"); -const isCronExpression = require('../lib/is-cron-expression'); +const isCronExpression = require("../lib/is-cron-expression"); -describe('Is cron expression', function () { - it('valid cron expressions', function () { - assert.equal(isCronExpression('* * * * * *'), true); - assert.equal(isCronExpression('1 * * * * *'), true); - assert.equal(isCronExpression('0 0 13-23 * * *'), true, 'Range should be 0-23'); +describe("Is cron expression", function () { + it("valid cron expressions", function () { + assert.equal(isCronExpression("* * * * * *"), true); + assert.equal(isCronExpression("1 * * * * *"), true); + assert.equal(isCronExpression("0 0 13-23 * * *"), true, "Range should be 0-23"); }); - it('invalid cron expressions', function () { - assert.equal(isCronExpression('0 123 * * * *'), false); - assert.equal(isCronExpression('a * * * *'), false); - assert.equal(isCronExpression('* 13-24 * * *'), false, 'Invalid range should be 0-23'); + it("invalid cron expressions", function () { + assert.equal(isCronExpression("0 123 * * * *"), false); + assert.equal(isCronExpression("a * * * *"), false); + assert.equal(isCronExpression("* 13-24 * * *"), false, "Invalid range should be 0-23"); }); - it('supports cron-validate default export shape', function () { - const rewiredModule = rewire('../lib/is-cron-expression'); + it("supports cron-validate default export shape", function () { + const rewiredModule = rewire("../lib/is-cron-expression"); let calledWith = null; - rewiredModule.__set__('cronValidate', (expression, options) => { - calledWith = {expression, options}; + rewiredModule.__set__("cronValidate", (expression, options) => { + calledWith = { expression, options }; return { isValid() { return true; - } + }, }; }); - assert.equal(rewiredModule('*/5 * * * * *'), true); + assert.equal(rewiredModule("*/5 * * * * *"), true); assert.deepEqual(calledWith, { - expression: '*/5 * * * * *', + expression: "*/5 * * * * *", options: { - preset: 'default', + preset: "default", override: { - useSeconds: true - } - } + useSeconds: true, + }, + }, }); }); }); diff --git a/packages/job-manager/test/job-manager.test.js b/packages/job-manager/test/job-manager.test.js index 97aa70dcd..433d86bde 100644 --- a/packages/job-manager/test/job-manager.test.js +++ b/packages/job-manager/test/job-manager.test.js @@ -1,41 +1,41 @@ -const assert = require('assert/strict'); -const path = require('path'); -const sinon = require('sinon'); -const {setTimeout: delay} = require('timers/promises'); -const FakeTimers = require('@sinonjs/fake-timers'); -const logging = require('@tryghost/logging'); +const assert = require("assert/strict"); +const path = require("path"); +const sinon = require("sinon"); +const { setTimeout: delay } = require("timers/promises"); +const FakeTimers = require("@sinonjs/fake-timers"); +const logging = require("@tryghost/logging"); -const JobManager = require('../index'); -const assembleBreeJob = require('../lib/assemble-bree-job'); -const JobsRepository = require('../lib/JobsRepository'); +const JobManager = require("../index"); +const assembleBreeJob = require("../lib/assemble-bree-job"); +const JobsRepository = require("../lib/JobsRepository"); const sandbox = sinon.createSandbox(); const jobModelInstance = { - id: 'unique', + id: "unique", get: (field) => { - if (field === 'status') { - return 'finished'; + if (field === "status") { + return "finished"; } - } + }, }; -describe('Job Manager', function () { +describe("Job Manager", function () { let stubConfig, jobManager; beforeEach(function () { - sandbox.stub(logging, 'info'); - sandbox.stub(logging, 'warn'); - sandbox.stub(logging, 'error'); + sandbox.stub(logging, "info"); + sandbox.stub(logging, "warn"); + sandbox.stub(logging, "error"); stubConfig = { get: sinon.stub().returns({ - enabled: true - }) + enabled: true, + }), }; jobManager = new JobManager({ - config: stubConfig + config: stubConfig, }); }); @@ -43,7 +43,7 @@ describe('Job Manager', function () { sandbox.restore(); }); - it('public interface', function () { + it("public interface", function () { assert.notEqual(jobManager.addJob, undefined); assert.notEqual(jobManager.hasExecutedSuccessfully, undefined); assert.notEqual(jobManager.awaitOneOffCompletion, undefined); @@ -54,14 +54,14 @@ describe('Job Manager', function () { assert.notEqual(jobManager.inlineJobHandler, undefined); }); - describe('Add a job', function () { - describe('Inline jobs', function () { - it('adds a job to a queue', async function () { + describe("Add a job", function () { + describe("Inline jobs", function () { + it("adds a job to a queue", async function () { const spy = sinon.spy(); jobManager.addJob({ job: spy, - data: 'test data', - offloaded: false + data: "test data", + offloaded: false, }); assert.equal(jobManager.inlineQueue.idle(), false); @@ -70,19 +70,19 @@ describe('Job Manager', function () { assert.equal(jobManager.inlineQueue.idle(), true); assert.equal(spy.called, true); - assert.equal(spy.args[0][0], 'test data'); + assert.equal(spy.args[0][0], "test data"); }); - it('handles failed job gracefully', async function () { + it("handles failed job gracefully", async function () { const spy = sinon.stub().throws(); const jobModelSpy = { - findOne: sinon.spy() + findOne: sinon.spy(), }; jobManager.addJob({ job: spy, - data: 'test data', - offloaded: false + data: "test data", + offloaded: false, }); assert.equal(jobManager.inlineQueue.idle(), false); @@ -91,66 +91,74 @@ describe('Job Manager', function () { assert.equal(jobManager.inlineQueue.idle(), true); assert.equal(spy.called, true); - assert.equal(spy.args[0][0], 'test data'); + assert.equal(spy.args[0][0], "test data"); assert.equal(logging.error.called, true); // a one-off job without a name should not have persistance assert.equal(jobModelSpy.findOne.called, false); }); }); - describe('Offloaded jobs', function () { - it('accepts cron schedule when worker scheduling is stubbed', async function () { - sandbox.stub(jobManager.bree, 'add').resolves(); - sandbox.stub(jobManager.bree, 'start').resolves(); + describe("Offloaded jobs", function () { + it("accepts cron schedule when worker scheduling is stubbed", async function () { + sandbox.stub(jobManager.bree, "add").resolves(); + sandbox.stub(jobManager.bree, "start").resolves(); - const jobPath = path.resolve(__dirname, './jobs/simple.js'); + const jobPath = path.resolve(__dirname, "./jobs/simple.js"); await jobManager.addJob({ - at: '* * * * * *', + at: "* * * * * *", job: jobPath, - name: 'cron-job' + name: "cron-job", }); assert.equal(jobManager.bree.add.called, true); assert.equal(jobManager.bree.start.called, true); }); - it('fails to schedule for invalid scheduling expression', async function () { - await assert.rejects(() => jobManager.addJob({ - at: 'invalid expression', - name: 'jobName' - }), {message: 'Invalid schedule format'}); + it("fails to schedule for invalid scheduling expression", async function () { + await assert.rejects( + () => + jobManager.addJob({ + at: "invalid expression", + name: "jobName", + }), + { message: "Invalid schedule format" }, + ); }); - it('fails to schedule for no job name', async function () { - await assert.rejects(() => jobManager.addJob({ - at: 'invalid expression', - job: () => {} - }), {message: 'Name parameter should be present if job is a function'}); + it("fails to schedule for no job name", async function () { + await assert.rejects( + () => + jobManager.addJob({ + at: "invalid expression", + job: () => {}, + }), + { message: "Name parameter should be present if job is a function" }, + ); }); - it('schedules a job using date format', async function () { + it("schedules a job using date format", async function () { const timeInTenSeconds = new Date(Date.now() + 10); - const jobPath = path.resolve(__dirname, './jobs/simple.js'); + const jobPath = path.resolve(__dirname, "./jobs/simple.js"); - const clock = FakeTimers.install({now: Date.now()}); + const clock = FakeTimers.install({ now: Date.now() }); try { await jobManager.addJob({ at: timeInTenSeconds, job: jobPath, - name: 'job-in-ten' + name: "job-in-ten", }); - assert.equal(jobManager.bree.timeouts.has('job-in-ten'), true); - assert.equal(jobManager.bree.workers.has('job-in-ten'), false); + assert.equal(jobManager.bree.timeouts.has("job-in-ten"), true); + assert.equal(jobManager.bree.workers.has("job-in-ten"), false); // allow to run the job and start the worker await clock.nextAsync(); - assert.equal(jobManager.bree.workers.has('job-in-ten'), true); + assert.equal(jobManager.bree.workers.has("job-in-ten"), true); const promise = new Promise((resolve, reject) => { - jobManager.bree.workers.get('job-in-ten').on('error', reject); - jobManager.bree.workers.get('job-in-ten').on('exit', (code) => { + jobManager.bree.workers.get("job-in-ten").on("error", reject); + jobManager.bree.workers.get("job-in-ten").on("exit", (code) => { assert.equal(code, 0); resolve(); }); @@ -161,31 +169,31 @@ describe('Job Manager', function () { await promise; - assert.equal(jobManager.bree.workers.has('job-in-ten'), false); + assert.equal(jobManager.bree.workers.has("job-in-ten"), false); } finally { clock.uninstall(); } }); - it('schedules a job to run immediately', async function () { - const clock = FakeTimers.install({now: Date.now()}); + it("schedules a job to run immediately", async function () { + const clock = FakeTimers.install({ now: Date.now() }); try { - const jobPath = path.resolve(__dirname, './jobs/simple.js'); + const jobPath = path.resolve(__dirname, "./jobs/simple.js"); await jobManager.addJob({ job: jobPath, - name: 'job-now' + name: "job-now", }); - assert.equal(jobManager.bree.timeouts.has('job-now'), true); + assert.equal(jobManager.bree.timeouts.has("job-now"), true); // allow scheduler to pick up the job await clock.tickAsync(1); - assert.equal(jobManager.bree.workers.has('job-now'), true); + assert.equal(jobManager.bree.workers.has("job-now"), true); const promise = new Promise((resolve, reject) => { - jobManager.bree.workers.get('job-now').on('error', reject); - jobManager.bree.workers.get('job-now').on('exit', (code) => { + jobManager.bree.workers.get("job-now").on("error", reject); + jobManager.bree.workers.get("job-now").on("exit", (code) => { assert.equal(code, 0); resolve(); }); @@ -193,31 +201,31 @@ describe('Job Manager', function () { await promise; - assert.equal(jobManager.bree.workers.has('job-now'), false); + assert.equal(jobManager.bree.workers.has("job-now"), false); } finally { clock.uninstall(); } }); - it('fails to schedule a job with the same name to run immediately one after another', async function () { - const clock = FakeTimers.install({now: Date.now()}); + it("fails to schedule a job with the same name to run immediately one after another", async function () { + const clock = FakeTimers.install({ now: Date.now() }); try { - const jobPath = path.resolve(__dirname, './jobs/simple.js'); + const jobPath = path.resolve(__dirname, "./jobs/simple.js"); await jobManager.addJob({ job: jobPath, - name: 'job-now' + name: "job-now", }); - assert.equal(jobManager.bree.timeouts.has('job-now'), true); + assert.equal(jobManager.bree.timeouts.has("job-now"), true); // allow scheduler to pick up the job await clock.tickAsync(1); - assert.equal(jobManager.bree.workers.has('job-now'), true); + assert.equal(jobManager.bree.workers.has("job-now"), true); const promise = new Promise((resolve, reject) => { - jobManager.bree.workers.get('job-now').on('error', reject); - jobManager.bree.workers.get('job-now').on('exit', (code) => { + jobManager.bree.workers.get("job-now").on("error", reject); + jobManager.bree.workers.get("job-now").on("exit", (code) => { assert.equal(code, 0); resolve(); }); @@ -225,88 +233,98 @@ describe('Job Manager', function () { await promise; - assert.equal(jobManager.bree.workers.has('job-now'), false); + assert.equal(jobManager.bree.workers.has("job-now"), false); - await assert.rejects(() => jobManager.addJob({ - job: jobPath, - name: 'job-now' - }), /Job #1 has a duplicate job name of job-now/); + await assert.rejects( + () => + jobManager.addJob({ + job: jobPath, + name: "job-now", + }), + /Job #1 has a duplicate job name of job-now/, + ); } finally { clock.uninstall(); } }); - it('uses custom error handler when job fails', async function (){ + it("uses custom error handler when job fails", async function () { let job = function namedJob() { - throw new Error('job error'); + throw new Error("job error"); }; const spyHandler = sinon.spy(); - jobManager = new JobManager({errorHandler: spyHandler, config: stubConfig}); - const completion = jobManager.awaitCompletion('will-fail'); + jobManager = new JobManager({ errorHandler: spyHandler, config: stubConfig }); + const completion = jobManager.awaitCompletion("will-fail"); await jobManager.addJob({ job, - name: 'will-fail' + name: "will-fail", }); await assert.rejects(completion, /job error/); assert.equal(spyHandler.called, true); - assert.equal(spyHandler.args[0][0].message, 'job error'); - assert.equal(spyHandler.args[0][1].name, 'will-fail'); + assert.equal(spyHandler.args[0][0].message, "job error"); + assert.equal(spyHandler.args[0][1].name, "will-fail"); }); - it('uses worker message handler when job sends a message', async function (){ + it("uses worker message handler when job sends a message", async function () { const workerMessageHandlerSpy = sinon.spy(); - jobManager = new JobManager({workerMessageHandler: workerMessageHandlerSpy, config: stubConfig}); - const completion = jobManager.awaitCompletion('will-send-msg'); + jobManager = new JobManager({ + workerMessageHandler: workerMessageHandlerSpy, + config: stubConfig, + }); + const completion = jobManager.awaitCompletion("will-send-msg"); await jobManager.addJob({ - job: path.resolve(__dirname, './jobs/message.js'), - name: 'will-send-msg' + job: path.resolve(__dirname, "./jobs/message.js"), + name: "will-send-msg", }); - await jobManager.bree.run('will-send-msg'); + await jobManager.bree.run("will-send-msg"); await delay(100); - jobManager.bree.workers.get('will-send-msg').postMessage('hello from Ghost!'); + jobManager.bree.workers.get("will-send-msg").postMessage("hello from Ghost!"); await completion; assert.equal(workerMessageHandlerSpy.called, true); - assert.equal(workerMessageHandlerSpy.args[0][0].name, 'will-send-msg'); - assert.equal(workerMessageHandlerSpy.args[0][0].message, 'Worker received: hello from Ghost!'); + assert.equal(workerMessageHandlerSpy.args[0][0].name, "will-send-msg"); + assert.equal( + workerMessageHandlerSpy.args[0][0].message, + "Worker received: hello from Ghost!", + ); }); }); }); - describe('Add one off job', function () { - it('throws if name parameter is not provided', async function () { + describe("Add one off job", function () { + it("throws if name parameter is not provided", async function () { try { await jobManager.addOneOffJob({ - job: () => {} + job: () => {}, }); - throw new Error('should have thrown'); + throw new Error("should have thrown"); } catch (err) { - assert.equal(err.message, 'The name parameter is required for a one off job.'); + assert.equal(err.message, "The name parameter is required for a one off job."); } }); - describe('Inline jobs', function () { - it('can execute inline jobs provided as a module path', async function () { + describe("Inline jobs", function () { + it("can execute inline jobs provided as a module path", async function () { jobManager.addJob({ - job: path.resolve(__dirname, './jobs/inline-module.js'), - data: 'test data', - offloaded: false + job: path.resolve(__dirname, "./jobs/inline-module.js"), + data: "test data", + offloaded: false, }); await delay(10); assert.equal(jobManager.inlineQueue.idle(), true); }); - it('handles failing inline jobs provided as a module path', async function () { - const modulePath = path.resolve(__dirname, './jobs/inline-module-throws.js'); + it("handles failing inline jobs provided as a module path", async function () { + const modulePath = path.resolve(__dirname, "./jobs/inline-module-throws.js"); jobManager.addJob({ job: modulePath, - offloaded: false + offloaded: false, }); await delay(10); @@ -314,401 +332,422 @@ describe('Job Manager', function () { assert.equal(logging.error.called, true); }); - it('adds job to the queue when it is a unique one', async function () { + it("adds job to the queue when it is a unique one", async function () { const spy = sinon.spy(); const JobModel = { findOne: sinon.stub().resolves(undefined), - add: sinon.stub().resolves() + add: sinon.stub().resolves(), }; - jobManager = new JobManager({JobModel, config: stubConfig}); + jobManager = new JobManager({ JobModel, config: stubConfig }); await jobManager.addOneOffJob({ job: spy, - name: 'unique name', - data: 'test data', - offloaded: false + name: "unique name", + data: "test data", + offloaded: false, }); assert.equal(JobModel.add.called, true); }); - it('does not add a job to the queue when it already exists', async function () { + it("does not add a job to the queue when it already exists", async function () { const spy = sinon.spy(); const JobModel = { findOne: sinon.stub().resolves(jobModelInstance), - add: sinon.stub().throws('should not be called') + add: sinon.stub().throws("should not be called"), }; - jobManager = new JobManager({JobModel, config: stubConfig}); + jobManager = new JobManager({ JobModel, config: stubConfig }); try { await jobManager.addOneOffJob({ job: spy, - name: 'I am the only one', - data: 'test data', - offloaded: false + name: "I am the only one", + data: "test data", + offloaded: false, }); - throw new Error('should not reach this point'); + throw new Error("should not reach this point"); } catch (error) { - assert.equal(error.message, 'A "I am the only one" one off job has already been executed.'); + assert.equal( + error.message, + 'A "I am the only one" one off job has already been executed.', + ); } }); - it('sets a finished state on an inline job', async function () { + it("sets a finished state on an inline job", async function () { const JobModel = { - findOne: sinon.stub() + findOne: sinon + .stub() .onCall(0) .resolves(null) - .resolves({id: 'unique', name: 'successful-oneoff'}), - add: sinon.stub().resolves({name: 'successful-oneoff'}), - edit: sinon.stub().resolves({name: 'successful-oneoff'}) + .resolves({ id: "unique", name: "successful-oneoff" }), + add: sinon.stub().resolves({ name: "successful-oneoff" }), + edit: sinon.stub().resolves({ name: "successful-oneoff" }), }; - jobManager = new JobManager({JobModel, config: stubConfig}); - const completion = jobManager.awaitCompletion('successful-oneoff'); + jobManager = new JobManager({ JobModel, config: stubConfig }); + const completion = jobManager.awaitCompletion("successful-oneoff"); jobManager.addOneOffJob({ job: async () => { return await delay(10); }, - name: 'successful-oneoff', - offloaded: false + name: "successful-oneoff", + offloaded: false, }); await completion; // tracks the job queued - assert.equal(JobModel.add.args[0][0].status, 'queued'); - assert.equal(JobModel.add.args[0][0].name, 'successful-oneoff'); + assert.equal(JobModel.add.args[0][0].status, "queued"); + assert.equal(JobModel.add.args[0][0].name, "successful-oneoff"); // tracks the job started - assert.equal(JobModel.edit.args[0][0].status, 'started'); + assert.equal(JobModel.edit.args[0][0].status, "started"); assert.notEqual(JobModel.edit.args[0][0].started_at, undefined); - assert.equal(JobModel.edit.args[0][1].id, 'unique'); + assert.equal(JobModel.edit.args[0][1].id, "unique"); // tracks the job finish - assert.equal(JobModel.edit.args[1][0].status, 'finished'); + assert.equal(JobModel.edit.args[1][0].status, "finished"); assert.notEqual(JobModel.edit.args[1][0].finished_at, undefined); - assert.equal(JobModel.edit.args[1][1].id, 'unique'); + assert.equal(JobModel.edit.args[1][1].id, "unique"); }); - it('sets a failed state on a job', async function () { + it("sets a failed state on a job", async function () { const JobModel = { - findOne: sinon.stub() + findOne: sinon + .stub() .onCall(0) .resolves(null) - .resolves({id: 'unique', name: 'failed-oneoff'}), - add: sinon.stub().resolves({name: 'failed-oneoff'}), - edit: sinon.stub().resolves({name: 'failed-oneoff'}) + .resolves({ id: "unique", name: "failed-oneoff" }), + add: sinon.stub().resolves({ name: "failed-oneoff" }), + edit: sinon.stub().resolves({ name: "failed-oneoff" }), }; let job = function namedJob() { - throw new Error('job error'); + throw new Error("job error"); }; const spyHandler = sinon.spy(); - jobManager = new JobManager({errorHandler: spyHandler, JobModel, config: stubConfig}); - const completion = jobManager.awaitCompletion('failed-oneoff'); + jobManager = new JobManager({ + errorHandler: spyHandler, + JobModel, + config: stubConfig, + }); + const completion = jobManager.awaitCompletion("failed-oneoff"); await jobManager.addOneOffJob({ job, - name: 'failed-oneoff', - offloaded: false + name: "failed-oneoff", + offloaded: false, }); await assert.rejects(completion, /job error/); // tracks the job start - assert.equal(JobModel.edit.args[0][0].status, 'started'); + assert.equal(JobModel.edit.args[0][0].status, "started"); assert.notEqual(JobModel.edit.args[0][0].started_at, undefined); - assert.equal(JobModel.edit.args[0][1].id, 'unique'); + assert.equal(JobModel.edit.args[0][1].id, "unique"); // tracks the job failure - assert.equal(JobModel.edit.args[1][0].status, 'failed'); - assert.equal(JobModel.edit.args[1][1].id, 'unique'); + assert.equal(JobModel.edit.args[1][0].status, "failed"); + assert.equal(JobModel.edit.args[1][1].id, "unique"); }); - it('adds job to the queue after failing', async function () { + it("adds job to the queue after failing", async function () { const JobModel = { - findOne: sinon.stub() + findOne: sinon + .stub() .onCall(0) .resolves(null) .onCall(1) - .resolves({id: 'unique'}) + .resolves({ id: "unique" }) .resolves({ - id: 'unique', + id: "unique", get: (field) => { - if (field === 'status') { - return 'failed'; + if (field === "status") { + return "failed"; } - } + }, }), add: sinon.stub().resolves({}), - edit: sinon.stub().resolves() + edit: sinon.stub().resolves(), }; let job = function namedJob() { - throw new Error('job error'); + throw new Error("job error"); }; const spyHandler = sinon.spy(); - jobManager = new JobManager({errorHandler: spyHandler, JobModel, config: stubConfig}); - const completion1 = jobManager.awaitCompletion('failed-oneoff'); + jobManager = new JobManager({ + errorHandler: spyHandler, + JobModel, + config: stubConfig, + }); + const completion1 = jobManager.awaitCompletion("failed-oneoff"); await jobManager.addOneOffJob({ job, - name: 'failed-oneoff', - offloaded: false + name: "failed-oneoff", + offloaded: false, }); // give time to execute the job and fail await assert.rejects(completion1, /job error/); - assert.equal(JobModel.edit.args[1][0].status, 'failed'); + assert.equal(JobModel.edit.args[1][0].status, "failed"); // simulate process restart and "fresh" slate to add the job - jobManager.removeJob('failed-oneoff'); - const completion2 = jobManager.awaitCompletion('failed-oneoff'); + jobManager.removeJob("failed-oneoff"); + const completion2 = jobManager.awaitCompletion("failed-oneoff"); await jobManager.addOneOffJob({ job, - name: 'failed-oneoff', - offloaded: false + name: "failed-oneoff", + offloaded: false, }); // give time to execute the job and fail AGAIN await assert.rejects(completion2, /job error/); - assert.equal(JobModel.edit.args[3][0].status, 'started'); - assert.equal(JobModel.edit.args[4][0].status, 'failed'); + assert.equal(JobModel.edit.args[3][0].status, "started"); + assert.equal(JobModel.edit.args[4][0].status, "failed"); }); }); - describe('Offloaded jobs', function () { - it('adds job to the queue when it is a unique one', async function () { + describe("Offloaded jobs", function () { + it("adds job to the queue when it is a unique one", async function () { const spy = sinon.spy(); const JobModel = { findOne: sinon.stub().resolves(undefined), - add: sinon.stub().resolves() + add: sinon.stub().resolves(), }; - jobManager = new JobManager({JobModel, config: stubConfig}); + jobManager = new JobManager({ JobModel, config: stubConfig }); await jobManager.addOneOffJob({ job: spy, - name: 'unique name', - data: 'test data' + name: "unique name", + data: "test data", }); assert.equal(JobModel.add.called, true); }); - it('does not add a job to the queue when it already exists', async function () { + it("does not add a job to the queue when it already exists", async function () { const spy = sinon.spy(); const JobModel = { findOne: sinon.stub().resolves(jobModelInstance), - add: sinon.stub().throws('should not be called') + add: sinon.stub().throws("should not be called"), }; - jobManager = new JobManager({JobModel, config: stubConfig}); + jobManager = new JobManager({ JobModel, config: stubConfig }); try { await jobManager.addOneOffJob({ job: spy, - name: 'I am the only one', - data: 'test data' + name: "I am the only one", + data: "test data", }); - throw new Error('should not reach this point'); + throw new Error("should not reach this point"); } catch (error) { - assert.equal(error.message, 'A "I am the only one" one off job has already been executed.'); + assert.equal( + error.message, + 'A "I am the only one" one off job has already been executed.', + ); } }); - it('sets a finished state on a job', async function () { + it("sets a finished state on a job", async function () { const JobModel = { - findOne: sinon.stub() + findOne: sinon + .stub() .onCall(0) .resolves(null) - .resolves({id: 'unique', name: 'successful-oneoff'}), - add: sinon.stub().resolves({name: 'successful-oneoff'}), - edit: sinon.stub().resolves({name: 'successful-oneoff'}) + .resolves({ id: "unique", name: "successful-oneoff" }), + add: sinon.stub().resolves({ name: "successful-oneoff" }), + edit: sinon.stub().resolves({ name: "successful-oneoff" }), }; - jobManager = new JobManager({JobModel, config: stubConfig}); + jobManager = new JobManager({ JobModel, config: stubConfig }); - const jobCompletion = jobManager.awaitCompletion('successful-oneoff'); + const jobCompletion = jobManager.awaitCompletion("successful-oneoff"); await jobManager.addOneOffJob({ - job: path.resolve(__dirname, './jobs/message.js'), - name: 'successful-oneoff' + job: path.resolve(__dirname, "./jobs/message.js"), + name: "successful-oneoff", }); // allow job to get picked up and executed await delay(100); - jobManager.bree.workers.get('successful-oneoff').postMessage('be done!'); + jobManager.bree.workers.get("successful-oneoff").postMessage("be done!"); // allow the message to be passed around await jobCompletion; // tracks the job start - assert.equal(JobModel.edit.args[0][0].status, 'started'); + assert.equal(JobModel.edit.args[0][0].status, "started"); assert.notEqual(JobModel.edit.args[0][0].started_at, undefined); - assert.equal(JobModel.edit.args[0][1].id, 'unique'); + assert.equal(JobModel.edit.args[0][1].id, "unique"); // tracks the job finish - assert.equal(JobModel.edit.args[1][0].status, 'finished'); + assert.equal(JobModel.edit.args[1][0].status, "finished"); assert.notEqual(JobModel.edit.args[1][0].finished_at, undefined); - assert.equal(JobModel.edit.args[1][1].id, 'unique'); + assert.equal(JobModel.edit.args[1][1].id, "unique"); }); - it('handles a failed job', async function () { + it("handles a failed job", async function () { const JobModel = { - findOne: sinon.stub() - .onCall(0) - .resolves(null) - .resolves(jobModelInstance), - add: sinon.stub().resolves({name: 'failed-oneoff'}), - edit: sinon.stub().resolves({name: 'failed-oneoff'}) + findOne: sinon.stub().onCall(0).resolves(null).resolves(jobModelInstance), + add: sinon.stub().resolves({ name: "failed-oneoff" }), + edit: sinon.stub().resolves({ name: "failed-oneoff" }), }; let job = function namedJob() { - throw new Error('job error'); + throw new Error("job error"); }; const spyHandler = sinon.spy(); - jobManager = new JobManager({errorHandler: spyHandler, JobModel, config: stubConfig}); + jobManager = new JobManager({ + errorHandler: spyHandler, + JobModel, + config: stubConfig, + }); - const completion = jobManager.awaitCompletion('failed-oneoff'); + const completion = jobManager.awaitCompletion("failed-oneoff"); await jobManager.addOneOffJob({ job, - name: 'failed-oneoff' + name: "failed-oneoff", }); await assert.rejects(completion, /job error/); // still calls the original error handler assert.equal(spyHandler.called, true); - assert.equal(spyHandler.args[0][0].message, 'job error'); - assert.equal(spyHandler.args[0][1].name, 'failed-oneoff'); + assert.equal(spyHandler.args[0][0].message, "job error"); + assert.equal(spyHandler.args[0][1].name, "failed-oneoff"); // tracks the job start - assert.equal(JobModel.edit.args[0][0].status, 'started'); + assert.equal(JobModel.edit.args[0][0].status, "started"); assert.notEqual(JobModel.edit.args[0][0].started_at, undefined); - assert.equal(JobModel.edit.args[0][1].id, 'unique'); + assert.equal(JobModel.edit.args[0][1].id, "unique"); // tracks the job failure - assert.equal(JobModel.edit.args[1][0].status, 'failed'); - assert.equal(JobModel.edit.args[1][1].id, 'unique'); + assert.equal(JobModel.edit.args[1][0].status, "failed"); + assert.equal(JobModel.edit.args[1][1].id, "unique"); }); }); }); - describe('Job execution progress', function () { - it('returns false when persistence is not configured', async function () { - jobManager = new JobManager({config: stubConfig}); - const executed = await jobManager.hasExecutedSuccessfully('no-repo-job'); + describe("Job execution progress", function () { + it("returns false when persistence is not configured", async function () { + jobManager = new JobManager({ config: stubConfig }); + const executed = await jobManager.hasExecutedSuccessfully("no-repo-job"); assert.equal(executed, false); }); - it('checks if job has ever been executed', async function () { + it("checks if job has ever been executed", async function () { const JobModel = { - findOne: sinon.stub() - .withArgs('solovei') + findOne: sinon + .stub() + .withArgs("solovei") .onCall(0) .resolves(null) .onCall(1) .resolves({ - id: 'unique', + id: "unique", get: (field) => { - if (field === 'status') { - return 'finished'; + if (field === "status") { + return "finished"; } - } + }, }) .onCall(2) .resolves({ - id: 'unique', + id: "unique", get: (field) => { - if (field === 'status') { - return 'failed'; + if (field === "status") { + return "failed"; } - } - }) + }, + }), }; - jobManager = new JobManager({JobModel, config: stubConfig}); - let executed = await jobManager.hasExecutedSuccessfully('solovei'); + jobManager = new JobManager({ JobModel, config: stubConfig }); + let executed = await jobManager.hasExecutedSuccessfully("solovei"); assert.equal(executed, false); - executed = await jobManager.hasExecutedSuccessfully('solovei'); + executed = await jobManager.hasExecutedSuccessfully("solovei"); assert.equal(executed, true); - executed = await jobManager.hasExecutedSuccessfully('solovei'); + executed = await jobManager.hasExecutedSuccessfully("solovei"); assert.equal(executed, false); }); - it('can wait for job completion', async function () { + it("can wait for job completion", async function () { const spy = sinon.spy(); - let status = 'queued'; + let status = "queued"; const jobWithDelay = async () => { await delay(80); - status = 'finished'; + status = "finished"; spy(); }; const JobModel = { - findOne: sinon.stub() + findOne: sinon + .stub() // first call when adding a job - .withArgs('solovei') + .withArgs("solovei") .onCall(0) // first call when adding a job .resolves(null) .onCall(1) .resolves(null) .resolves({ - id: 'unique', - get: () => status + id: "unique", + get: () => status, }), - add: sinon.stub().resolves() + add: sinon.stub().resolves(), }; - jobManager = new JobManager({JobModel, config: stubConfig}); + jobManager = new JobManager({ JobModel, config: stubConfig }); await jobManager.addOneOffJob({ job: jobWithDelay, - name: 'solovei', - offloaded: false + name: "solovei", + offloaded: false, }); assert.equal(spy.called, false); - await jobManager.awaitOneOffCompletion('solovei'); + await jobManager.awaitOneOffCompletion("solovei"); assert.equal(spy.called, true); }); }); - describe('Remove a job', function () { - it('removes a scheduled job from the queue', async function () { - jobManager = new JobManager({config: stubConfig}); + describe("Remove a job", function () { + it("removes a scheduled job from the queue", async function () { + jobManager = new JobManager({ config: stubConfig }); const timeInTenSeconds = new Date(Date.now() + 10); - const jobPath = path.resolve(__dirname, './jobs/simple.js'); + const jobPath = path.resolve(__dirname, "./jobs/simple.js"); await jobManager.addJob({ at: timeInTenSeconds, job: jobPath, - name: 'job-in-ten' + name: "job-in-ten", }); - assert.equal(jobManager.bree.config.jobs[0].name, 'job-in-ten'); + assert.equal(jobManager.bree.config.jobs[0].name, "job-in-ten"); - await jobManager.removeJob('job-in-ten'); + await jobManager.removeJob("job-in-ten"); assert.equal(jobManager.bree.config.jobs[0], undefined); }); }); - describe('Shutdown', function () { - it('gracefully shuts down inline jobs', async function () { - jobManager = new JobManager({config: stubConfig}); + describe("Shutdown", function () { + it("gracefully shuts down inline jobs", async function () { + jobManager = new JobManager({ config: stubConfig }); jobManager.addJob({ - job: require('./jobs/timed-job'), + job: require("./jobs/timed-job"), data: 200, - offloaded: false + offloaded: false, }); assert.equal(jobManager.inlineQueue.idle(), false); @@ -718,12 +757,12 @@ describe('Job Manager', function () { assert.equal(jobManager.inlineQueue.idle(), true); }); - it('gracefully shuts down an interval job', async function () { - jobManager = new JobManager({config: stubConfig}); + it("gracefully shuts down an interval job", async function () { + jobManager = new JobManager({ config: stubConfig }); await jobManager.addJob({ - at: 'every 5 seconds', - job: path.resolve(__dirname, './jobs/graceful.js') + at: "every 5 seconds", + job: path.resolve(__dirname, "./jobs/graceful.js"), }); await delay(1); // let the job execution kick in @@ -737,21 +776,21 @@ describe('Job Manager', function () { assert.equal(jobManager.bree.intervals.size, 0); }); - it('gracefully shuts down the job queue worker pool'); + it("gracefully shuts down the job queue worker pool"); }); - describe('allSettled', function () { - it('resolves immediately when queue is idle', async function () { + describe("allSettled", function () { + it("resolves immediately when queue is idle", async function () { await assert.doesNotReject(() => jobManager.allSettled()); }); - it('resolves once queued inline job completes', async function () { + it("resolves once queued inline job completes", async function () { jobManager.addJob({ - name: 'inline-all-settled', + name: "inline-all-settled", job: async () => { await delay(10); }, - offloaded: false + offloaded: false, }); await assert.doesNotReject(() => jobManager.allSettled()); @@ -759,60 +798,60 @@ describe('Job Manager', function () { }); }); - describe('Unit helpers', function () { - it('_jobMessageHandler dispatches domain events from worker messages', async function () { + describe("Unit helpers", function () { + it("_jobMessageHandler dispatches domain events from worker messages", async function () { const domainEvents = { - dispatchRaw: sinon.spy() + dispatchRaw: sinon.spy(), }; - jobManager = new JobManager({config: stubConfig, domainEvents}); + jobManager = new JobManager({ config: stubConfig, domainEvents }); await jobManager._jobMessageHandler({ - name: 'event-job', + name: "event-job", message: { event: { - type: 'my-event', - data: {foo: 'bar'} - } - } + type: "my-event", + data: { foo: "bar" }, + }, + }, }); assert.equal(domainEvents.dispatchRaw.calledOnce, true); - assert.deepEqual(domainEvents.dispatchRaw.args[0], ['my-event', {foo: 'bar'}]); + assert.deepEqual(domainEvents.dispatchRaw.args[0], ["my-event", { foo: "bar" }]); }); - it('_jobErrorHandler rejects allSettled listeners', async function () { - sandbox.stub(jobManager.inlineQueue, 'idle').returns(false); + it("_jobErrorHandler rejects allSettled listeners", async function () { + sandbox.stub(jobManager.inlineQueue, "idle").returns(false); const all = jobManager.allSettled(); - await jobManager._jobErrorHandler(new Error('all failed'), {name: 'no-op'}); + await jobManager._jobErrorHandler(new Error("all failed"), { name: "no-op" }); await assert.rejects(all, /all failed/); }); - it('assembleBreeJob supports cron expressions', function () { - const job = assembleBreeJob('* * * * * *', '/tmp/job.js', {foo: 'bar'}, 'cron-job'); - assert.equal(job.cron, '* * * * * *'); + it("assembleBreeJob supports cron expressions", function () { + const job = assembleBreeJob("* * * * * *", "/tmp/job.js", { foo: "bar" }, "cron-job"); + assert.equal(job.cron, "* * * * * *"); assert.equal(job.interval, undefined); assert.equal(job.date, undefined); }); - it('JobsRepository delete handles model delete errors', async function () { + it("JobsRepository delete handles model delete errors", async function () { const JobModel = { - destroy: sinon.stub().rejects(new Error('destroy failed')) + destroy: sinon.stub().rejects(new Error("destroy failed")), }; - const repository = new JobsRepository({JobModel}); + const repository = new JobsRepository({ JobModel }); - await assert.doesNotReject(() => repository.delete('abc')); + await assert.doesNotReject(() => repository.delete("abc")); assert.equal(logging.error.called, true); }); - it('JobsRepository delete resolves when model delete succeeds', async function () { + it("JobsRepository delete resolves when model delete succeeds", async function () { const JobModel = { - destroy: sinon.stub().resolves() + destroy: sinon.stub().resolves(), }; - const repository = new JobsRepository({JobModel}); + const repository = new JobsRepository({ JobModel }); - await assert.doesNotReject(() => repository.delete('abc')); + await assert.doesNotReject(() => repository.delete("abc")); assert.equal(JobModel.destroy.calledOnce, true); }); }); diff --git a/packages/job-manager/test/jobs/graceful.js b/packages/job-manager/test/jobs/graceful.js index 3333e3c32..47d4fe920 100644 --- a/packages/job-manager/test/jobs/graceful.js +++ b/packages/job-manager/test/jobs/graceful.js @@ -1,31 +1,31 @@ /* eslint-disable no-console */ -const setTimeoutPromise = require('util').promisify(setTimeout); -const {isMainThread, parentPort} = require('worker_threads'); +const setTimeoutPromise = require("util").promisify(setTimeout); +const { isMainThread, parentPort } = require("worker_threads"); let shutdown = false; if (!isMainThread) { - parentPort.on('message', (message) => { + parentPort.on("message", (message) => { console.log(`parent message received: ${message}`); // 'cancel' event is triggered when job has to to terminated before it finishes execution // usually it would come in when SIGINT signal is sent to a parent process - if (message === 'cancel') { + if (message === "cancel") { shutdown = true; } }); } (async () => { - console.log('started graceful job'); + console.log("started graceful job"); for (;;) { await setTimeoutPromise(1000); - console.log('worked for 1000 ms'); + console.log("worked for 1000 ms"); if (shutdown) { - console.log('exiting gracefully'); + console.log("exiting gracefully"); await setTimeoutPromise(100); // async cleanup imitation @@ -39,7 +39,7 @@ if (!isMainThread) { // because of parent initiated reason (e.g.: parent process interuption) // differs from 'done' by producing different // logging - shows the job was cancelled instead of completing - parentPort.postMessage('done'); + parentPort.postMessage("done"); // parentPort.postMessage('cancelled'); } else { process.exit(0); diff --git a/packages/job-manager/test/jobs/inline-module-throws.js b/packages/job-manager/test/jobs/inline-module-throws.js index a1457b81a..40824973a 100644 --- a/packages/job-manager/test/jobs/inline-module-throws.js +++ b/packages/job-manager/test/jobs/inline-module-throws.js @@ -1,3 +1,3 @@ module.exports = async function runInlineModuleThrowJob() { - throw new Error('inline module failure'); + throw new Error("inline module failure"); }; diff --git a/packages/job-manager/test/jobs/message.js b/packages/job-manager/test/jobs/message.js index d5e323f7a..f32d80745 100644 --- a/packages/job-manager/test/jobs/message.js +++ b/packages/job-manager/test/jobs/message.js @@ -1,20 +1,20 @@ -const {parentPort} = require('worker_threads'); +const { parentPort } = require("worker_threads"); -setInterval(() => { }, 10); +setInterval(() => {}, 10); if (parentPort) { - parentPort.on('message', (message) => { - if (message === 'error') { - throw new Error('oops'); + parentPort.on("message", (message) => { + if (message === "error") { + throw new Error("oops"); } - if (message === 'cancel') { - parentPort.postMessage('cancelled'); + if (message === "cancel") { + parentPort.postMessage("cancelled"); return; } // post the message back parentPort.postMessage(`Worker received: ${message}`); - parentPort.postMessage('done'); + parentPort.postMessage("done"); }); } diff --git a/packages/job-manager/test/jobs/timed-job.js b/packages/job-manager/test/jobs/timed-job.js index ec1575449..0c19f01bd 100644 --- a/packages/job-manager/test/jobs/timed-job.js +++ b/packages/job-manager/test/jobs/timed-job.js @@ -1,5 +1,5 @@ -const {isMainThread, parentPort, workerData} = require('worker_threads'); -const util = require('util'); +const { isMainThread, parentPort, workerData } = require("worker_threads"); +const util = require("util"); const setTimeoutPromise = util.promisify(setTimeout); const passTime = async (ms) => { @@ -15,7 +15,7 @@ if (isMainThread) { } else { (async () => { await passTime(workerData.ms); - parentPort.postMessage('done'); + parentPort.postMessage("done"); // alternative way to signal "finished" work (not recommended) // process.exit(); })(); diff --git a/packages/job-manager/vitest.config.ts b/packages/job-manager/vitest.config.ts index 601b1a6ad..3cd67834a 100644 --- a/packages/job-manager/vitest.config.ts +++ b/packages/job-manager/vitest.config.ts @@ -1,11 +1,14 @@ -import {defineConfig, mergeConfig} from 'vitest/config'; -import rootConfig from '../../vitest.config'; +import { defineConfig, mergeConfig } from "vitest/config"; +import rootConfig from "../../vitest.config"; // Override: Bree spawns background workers that emit unhandled rejections // during cleanup after tests complete. These are expected and were silently // ignored by Mocha. -export default mergeConfig(rootConfig, defineConfig({ - test: { - dangerouslyIgnoreUnhandledErrors: true - } -})); +export default mergeConfig( + rootConfig, + defineConfig({ + test: { + dangerouslyIgnoreUnhandledErrors: true, + }, + }), +); diff --git a/packages/logging/README.md b/packages/logging/README.md index 0a842a9fa..fa99c3d4b 100644 --- a/packages/logging/README.md +++ b/packages/logging/README.md @@ -8,36 +8,30 @@ or `yarn add @tryghost/logging` - ## Purpose Ghost logging layer that configures logger instances, transports, and structured log formatting. ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests - - - -# Copyright & License +# Copyright & License Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/logging/index.js b/packages/logging/index.js index 5f3aa1e0e..8deddf589 100644 --- a/packages/logging/index.js +++ b/packages/logging/index.js @@ -1 +1 @@ -module.exports = require('./lib/logging'); +module.exports = require("./lib/logging"); diff --git a/packages/logging/lib/GhostLogger.js b/packages/logging/lib/GhostLogger.js index cabfddeba..5634393e8 100644 --- a/packages/logging/lib/GhostLogger.js +++ b/packages/logging/lib/GhostLogger.js @@ -1,12 +1,12 @@ -const each = require('lodash/each'); -const upperFirst = require('lodash/upperFirst'); -const toArray = require('lodash/toArray'); -const isObject = require('lodash/isObject'); -const isEmpty = require('lodash/isEmpty'); -const includes = require('lodash/includes'); -const bunyan = require('bunyan'); -const fs = require('fs'); -const jsonStringifySafe = require('json-stringify-safe'); +const each = require("lodash/each"); +const upperFirst = require("lodash/upperFirst"); +const toArray = require("lodash/toArray"); +const isObject = require("lodash/isObject"); +const isEmpty = require("lodash/isEmpty"); +const includes = require("lodash/includes"); +const bunyan = require("bunyan"); +const fs = require("fs"); +const jsonStringifySafe = require("json-stringify-safe"); /** * @description Ghost's logger class. @@ -39,15 +39,15 @@ class GhostLogger { constructor(options) { options = options || {}; - this.name = options.name || 'Log'; - this.env = options.env || 'development'; - this.domain = options.domain || 'localhost'; - this.transports = options.transports || ['stdout']; - this.level = process.env.LEVEL || options.level || 'info'; + this.name = options.name || "Log"; + this.env = options.env || "development"; + this.domain = options.domain || "localhost"; + this.transports = options.transports || ["stdout"]; + this.level = process.env.LEVEL || options.level || "info"; this.logBody = options.logBody || false; - this.mode = process.env.MODE || options.mode || 'short'; + this.mode = process.env.MODE || options.mode || "short"; this.path = options.path || process.cwd(); - this.filename = options.filename || '{domain}_{env}'; + this.filename = options.filename || "{domain}_{env}"; this.loggly = options.loggly || {}; this.elasticsearch = options.elasticsearch || {}; this.gelf = options.gelf || {}; @@ -56,33 +56,33 @@ class GhostLogger { this.metadata = options.metadata || {}; // CASE: stdout has to be on the first position in the transport, because if the GhostLogger itself logs, you won't see the stdout print - if (this.transports.indexOf('stdout') !== -1 && this.transports.indexOf('stdout') !== 0) { - this.transports.splice(this.transports.indexOf('stdout'), 1); - this.transports = ['stdout'].concat(this.transports); + if (this.transports.indexOf("stdout") !== -1 && this.transports.indexOf("stdout") !== 0) { + this.transports.splice(this.transports.indexOf("stdout"), 1); + this.transports = ["stdout"].concat(this.transports); } // CASE: special env variable to enable long mode and level info if (process.env.LOIN) { - this.level = 'info'; - this.mode = 'long'; + this.level = "info"; + this.mode = "long"; } // CASE: ensure we have a trailing slash if (!this.path.match(/\/$|\\$/)) { - this.path = this.path + '/'; + this.path = this.path + "/"; } this.rotation = options.rotation || { enabled: false, - period: '1w', - count: 100 + period: "1w", + count: 100, }; this.streams = {}; this.setSerializers(); - if (includes(this.transports, 'stderr') && !includes(this.transports, 'stdout')) { - this.transports.push('stdout'); + if (includes(this.transports, "stderr") && !includes(this.transports, "stdout")) { + this.transports.push("stdout"); } this.transports.forEach((transport) => { @@ -100,24 +100,26 @@ class GhostLogger { * @description Setup stdout stream. */ setStdoutStream() { - const GhostPrettyStream = require('@tryghost/pretty-stream'); + const GhostPrettyStream = require("@tryghost/pretty-stream"); const prettyStdOut = new GhostPrettyStream({ - mode: this.mode + mode: this.mode, }); prettyStdOut.pipe(process.stdout); this.streams.stdout = { - name: 'stdout', + name: "stdout", log: bunyan.createLogger({ name: this.name, - streams: [{ - type: 'raw', - stream: prettyStdOut, - level: this.level - }], - serializers: this.serializers - }) + streams: [ + { + type: "raw", + stream: prettyStdOut, + level: this.level, + }, + ], + serializers: this.serializers, + }), }; } @@ -125,24 +127,26 @@ class GhostLogger { * @description Setup stderr stream. */ setStderrStream() { - const GhostPrettyStream = require('@tryghost/pretty-stream'); + const GhostPrettyStream = require("@tryghost/pretty-stream"); const prettyStdErr = new GhostPrettyStream({ - mode: this.mode + mode: this.mode, }); prettyStdErr.pipe(process.stderr); this.streams.stderr = { - name: 'stderr', + name: "stderr", log: bunyan.createLogger({ name: this.name, - streams: [{ - type: 'raw', - stream: prettyStdErr, - level: 'error' - }], - serializers: this.serializers - }) + streams: [ + { + type: "raw", + stream: prettyStdErr, + level: "error", + }, + ], + serializers: this.serializers, + }), }; } @@ -150,26 +154,28 @@ class GhostLogger { * Setup stream for posting the message to a parent instance */ setParentStream() { - const {parentPort} = require('worker_threads'); + const { parentPort } = require("worker_threads"); const bunyanStream = { // Parent stream only supports sending a string write: (bunyanObject) => { - const {msg} = bunyanObject; + const { msg } = bunyanObject; parentPort.postMessage(msg); - } + }, }; this.streams.parent = { - name: 'parent', + name: "parent", log: bunyan.createLogger({ name: this.name, - streams: [{ - type: 'raw', - stream: bunyanStream, - level: this.level - }], - serializers: this.serializers - }) + streams: [ + { + type: "raw", + stream: bunyanStream, + level: this.level, + }, + ], + serializers: this.serializers, + }), }; } @@ -177,26 +183,28 @@ class GhostLogger { * @description Setup loggly. */ setLogglyStream() { - const Bunyan2Loggly = require('bunyan-loggly'); + const Bunyan2Loggly = require("bunyan-loggly"); const logglyStream = new Bunyan2Loggly({ token: this.loggly.token, subdomain: this.loggly.subdomain, - tags: this.loggly.tags + tags: this.loggly.tags, }); this.streams.loggly = { - name: 'loggly', + name: "loggly", match: this.loggly.match, log: bunyan.createLogger({ name: this.name, - streams: [{ - type: 'raw', - stream: logglyStream, - level: 'error' - }], - serializers: this.serializers - }) + streams: [ + { + type: "raw", + stream: logglyStream, + level: "error", + }, + ], + serializers: this.serializers, + }), }; } @@ -204,51 +212,59 @@ class GhostLogger { * @description Setup ElasticSearch. */ setElasticsearchStream() { - const ElasticSearch = require('@tryghost/elasticsearch').BunyanStream; - - const elasticSearchInstance = new ElasticSearch({ - node: this.elasticsearch.host, - auth: { - username: this.elasticsearch.username, - password: this.elasticsearch.password - } - }, this.elasticsearch.index, this.elasticsearch.pipeline); + const ElasticSearch = require("@tryghost/elasticsearch").BunyanStream; + + const elasticSearchInstance = new ElasticSearch( + { + node: this.elasticsearch.host, + auth: { + username: this.elasticsearch.username, + password: this.elasticsearch.password, + }, + }, + this.elasticsearch.index, + this.elasticsearch.pipeline, + ); this.streams.elasticsearch = { - name: 'elasticsearch', + name: "elasticsearch", log: bunyan.createLogger({ name: this.name, - streams: [{ - type: 'stream', - stream: elasticSearchInstance.getStream(), - level: this.elasticsearch.level - }], - serializers: this.serializers - }) + streams: [ + { + type: "stream", + stream: elasticSearchInstance.getStream(), + level: this.elasticsearch.level, + }, + ], + serializers: this.serializers, + }), }; } setHttpStream() { - const Http = require('@tryghost/http-stream'); + const Http = require("@tryghost/http-stream"); const httpStream = new Http({ url: this.http.url, headers: this.http.headers || {}, - username: this.http.username || '', - password: this.http.password || '' + username: this.http.username || "", + password: this.http.password || "", }); this.streams.http = { - name: 'http', + name: "http", log: bunyan.createLogger({ name: this.name, - streams: [{ - type: 'raw', - stream: httpStream, - level: this.http.level - }], - serializers: this.serializers - }) + streams: [ + { + type: "raw", + stream: httpStream, + level: this.http.level, + }, + ], + serializers: this.serializers, + }), }; } @@ -256,25 +272,27 @@ class GhostLogger { * @description Setup gelf. */ setGelfStream() { - const gelfStream = require('gelf-stream'); + const gelfStream = require("gelf-stream"); const stream = gelfStream.forBunyan( - this.gelf.host || 'localhost', + this.gelf.host || "localhost", this.gelf.port || 12201, - this.gelf.options || {} + this.gelf.options || {}, ); this.streams.gelf = { - name: 'gelf', + name: "gelf", log: bunyan.createLogger({ name: this.name, - streams: [{ - type: 'raw', - stream: stream, - level: this.level - }], - serializers: this.serializers - }) + streams: [ + { + type: "raw", + stream: stream, + level: this.level, + }, + ], + serializers: this.serializers, + }), }; } @@ -287,7 +305,7 @@ class GhostLogger { * sanitizeDomain('http://my-domain.com') // returns 'http___my_domain_com' */ sanitizeDomain(domain) { - return domain.replace(/[^\w]/gi, '_'); + return domain.replace(/[^\w]/gi, "_"); } /** @@ -314,102 +332,119 @@ class GhostLogger { // CASE: target log folder does not exist, show warning if (!fs.existsSync(this.path)) { - this.error('Target log folder does not exist: ' + this.path); + this.error("Target log folder does not exist: " + this.path); return; } if (this.rotation.enabled) { if (this.rotation.useLibrary) { - const RotatingFileStream = require('@tryghost/bunyan-rotating-filestream'); + const RotatingFileStream = require("@tryghost/bunyan-rotating-filestream"); const rotationConfig = { path: `${this.path}${baseFilename}.log`, period: this.rotation.period, threshold: this.rotation.threshold, totalFiles: this.rotation.count, gzip: this.rotation.gzip, - rotateExisting: (typeof this.rotation.rotateExisting === 'undefined') ? this.rotation.rotateExisting : true + rotateExisting: + typeof this.rotation.rotateExisting === "undefined" + ? this.rotation.rotateExisting + : true, }; - this.streams['rotation-errors'] = { - name: 'rotation-errors', + this.streams["rotation-errors"] = { + name: "rotation-errors", log: bunyan.createLogger({ name: this.name, - streams: [{ - stream: new RotatingFileStream(Object.assign({}, rotationConfig, { - path: `${this.path}${baseFilename}.error.log` - })), - level: 'error' - }], - serializers: this.serializers - }) + streams: [ + { + stream: new RotatingFileStream( + Object.assign({}, rotationConfig, { + path: `${this.path}${baseFilename}.error.log`, + }), + ), + level: "error", + }, + ], + serializers: this.serializers, + }), }; - this.streams['rotation-all'] = { - name: 'rotation-all', + this.streams["rotation-all"] = { + name: "rotation-all", log: bunyan.createLogger({ name: this.name, - streams: [{ - stream: new RotatingFileStream(rotationConfig), - level: this.level - }], - serializers: this.serializers - }) + streams: [ + { + stream: new RotatingFileStream(rotationConfig), + level: this.level, + }, + ], + serializers: this.serializers, + }), }; } else { // TODO: Remove this when confidence is high in the external library for rotation - this.streams['rotation-errors'] = { - name: 'rotation-errors', + this.streams["rotation-errors"] = { + name: "rotation-errors", log: bunyan.createLogger({ name: this.name, - streams: [{ - type: 'rotating-file', - path: `${this.path}${baseFilename}.error.log`, - period: this.rotation.period, - count: this.rotation.count, - level: 'error' - }], - serializers: this.serializers - }) + streams: [ + { + type: "rotating-file", + path: `${this.path}${baseFilename}.error.log`, + period: this.rotation.period, + count: this.rotation.count, + level: "error", + }, + ], + serializers: this.serializers, + }), }; - this.streams['rotation-all'] = { - name: 'rotation-all', + this.streams["rotation-all"] = { + name: "rotation-all", log: bunyan.createLogger({ name: this.name, - streams: [{ - type: 'rotating-file', - path: `${this.path}${baseFilename}.log`, - period: this.rotation.period, - count: this.rotation.count, - level: this.level - }], - serializers: this.serializers - }) + streams: [ + { + type: "rotating-file", + path: `${this.path}${baseFilename}.log`, + period: this.rotation.period, + count: this.rotation.count, + level: this.level, + }, + ], + serializers: this.serializers, + }), }; } } else { - this.streams['file-errors'] = { - name: 'file', + this.streams["file-errors"] = { + name: "file", log: bunyan.createLogger({ name: this.name, - streams: [{ - path: `${this.path}${baseFilename}.error.log`, - level: 'error' - }], - serializers: this.serializers - }) + streams: [ + { + path: `${this.path}${baseFilename}.error.log`, + level: "error", + }, + ], + serializers: this.serializers, + }), }; - this.streams['file-all'] = { - name: 'file', + this.streams["file-all"] = { + name: "file", log: bunyan.createLogger({ name: this.name, - streams: [{ - path: `${this.path}${baseFilename}.log`, - level: this.level - }], - serializers: this.serializers - }) + streams: [ + { + path: `${this.path}${baseFilename}.log`, + level: this.level, + }, + ], + serializers: this.serializers, + }), }; } } @@ -428,14 +463,14 @@ class GhostLogger { const requestLog = { meta: { requestId: req.requestId, - userId: req.userId + userId: req.userId, }, url: req.url, method: req.method, originalUrl: req.originalUrl, params: req.params, headers: this.removeSensitiveData(req.headers), - query: this.removeSensitiveData(req.query) + query: this.removeSensitiveData(req.query), }; if (req.extra) { @@ -456,7 +491,7 @@ class GhostLogger { return { _headers: this.removeSensitiveData(res.getHeaders()), statusCode: res.statusCode, - responseTime: res.responseTime + responseTime: res.responseTime, }; }, err: (err) => { @@ -472,9 +507,9 @@ class GhostLogger { help: jsonStringifySafe(err.help), stack: err.stack, hideStack: err.hideStack, - errorDetails: jsonStringifySafe(err.errorDetails) + errorDetails: jsonStringifySafe(err.errorDetails), }; - } + }, }; } @@ -493,7 +528,7 @@ class GhostLogger { } if (key.match(/pin|password|pass|key|authorization|bearer|cookie/gi)) { - newObj[key] = '**REDACTED**'; + newObj[key] = "**REDACTED**"; } else { newObj[key] = value; } @@ -556,7 +591,11 @@ class GhostLogger { each(this.streams, (logger) => { // If we have both a stdout and a stderr stream, don't log errors to stdout // because it would result in duplicate logs - if (type === 'error' && logger.name === 'stdout' && includes(this.transports, 'stderr')) { + if ( + type === "error" && + logger.name === "stdout" && + includes(this.transports, "stderr") + ) { return; } @@ -580,8 +619,12 @@ class GhostLogger { * `jsonStringifySafe` can match a string in an object, which has circular dependencies. * https://github.com/moll/json-stringify-safe */ - if (logger.match && type === 'error') { - if (new RegExp(logger.match).test(jsonStringifySafe(modifiedArguments[0].err || null).replace(/"/g, ''))) { + if (logger.match && type === "error") { + if ( + new RegExp(logger.match).test( + jsonStringifySafe(modifiedArguments[0].err || null).replace(/"/g, ""), + ) + ) { logger.log[type](...modifiedArguments); } } else { @@ -591,27 +634,27 @@ class GhostLogger { } trace() { - this.log('trace', toArray(arguments)); + this.log("trace", toArray(arguments)); } debug() { - this.log('debug', toArray(arguments)); + this.log("debug", toArray(arguments)); } info() { - this.log('info', toArray(arguments)); + this.log("info", toArray(arguments)); } warn() { - this.log('warn', toArray(arguments)); + this.log("warn", toArray(arguments)); } error() { - this.log('error', toArray(arguments)); + this.log("error", toArray(arguments)); } fatal() { - this.log('fatal', toArray(arguments)); + this.log("fatal", toArray(arguments)); } /** @@ -625,13 +668,13 @@ class GhostLogger { transports: [], level: this.level, logBody: this.logBody, - mode: this.mode + mode: this.mode, }); result.streams = Object.keys(this.streams).reduce((acc, id) => { acc[id] = { name: this.streams[id].name, - log: this.streams[id].log.child(boundProperties) + log: this.streams[id].log.child(boundProperties), }; return acc; }, {}); diff --git a/packages/logging/lib/logging.js b/packages/logging/lib/logging.js index c783e0a70..34e142689 100644 --- a/packages/logging/lib/logging.js +++ b/packages/logging/lib/logging.js @@ -1,17 +1,17 @@ -const path = require('path'); -const {isMainThread} = require('worker_threads'); -const {getProcessRoot} = require('@tryghost/root-utils'); -const GhostLogger = require('./GhostLogger'); +const path = require("path"); +const { isMainThread } = require("worker_threads"); +const { getProcessRoot } = require("@tryghost/root-utils"); +const GhostLogger = require("./GhostLogger"); let loggingConfig; try { - loggingConfig = require(path.join(getProcessRoot(), 'loggingrc')); + loggingConfig = require(path.join(getProcessRoot(), "loggingrc")); } catch (err) { loggingConfig = {}; } if (!isMainThread) { - loggingConfig.transports = ['parent']; + loggingConfig.transports = ["parent"]; } module.exports = new GhostLogger(loggingConfig); diff --git a/packages/logging/package.json b/packages/logging/package.json index bec497e5e..a63582490 100644 --- a/packages/logging/package.json +++ b/packages/logging/package.json @@ -1,31 +1,27 @@ { "name": "@tryghost/logging", "version": "4.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/logging" }, - "author": "Ghost Foundation", - "license": "MIT", - "main": "index.js", - "scripts": { - "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" - }, "files": [ "index.js", "lib" ], + "main": "index.js", "publishConfig": { "access": "public" }, - "devDependencies": { - "@tryghost/errors": "1.0.0", - "sinon": "21.0.3" + "scripts": { + "dev": "echo \"Implement me!\"", + "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "dependencies": { "@tryghost/bunyan-rotating-filestream": "0.0.7", @@ -39,5 +35,9 @@ "gelf-stream": "1.1.1", "json-stringify-safe": "5.0.1", "lodash": "4.17.23" + }, + "devDependencies": { + "@tryghost/errors": "1.0.0", + "sinon": "21.0.3" } } diff --git a/packages/logging/test/.eslintrc.js b/packages/logging/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/logging/test/.eslintrc.js +++ b/packages/logging/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/logging/test/logging.test.js b/packages/logging/test/logging.test.js index 0c997e40d..ac35e5dcc 100644 --- a/packages/logging/test/logging.test.js +++ b/packages/logging/test/logging.test.js @@ -1,66 +1,66 @@ -const fs = require('fs'); -const PrettyStream = require('@tryghost/pretty-stream'); -const GhostLogger = require('../lib/GhostLogger'); -const includes = require('lodash/includes'); -const errors = require('@tryghost/errors'); -const sinon = require('sinon'); -const assert = require('assert/strict'); -const Bunyan2Loggly = require('bunyan-loggly'); -const GelfStream = require('gelf-stream').GelfStream; -const ElasticSearch = require('@tryghost/elasticsearch').BunyanStream; -const HttpStream = require('@tryghost/http-stream'); +const fs = require("fs"); +const PrettyStream = require("@tryghost/pretty-stream"); +const GhostLogger = require("../lib/GhostLogger"); +const includes = require("lodash/includes"); +const errors = require("@tryghost/errors"); +const sinon = require("sinon"); +const assert = require("assert/strict"); +const Bunyan2Loggly = require("bunyan-loggly"); +const GelfStream = require("gelf-stream").GelfStream; +const ElasticSearch = require("@tryghost/elasticsearch").BunyanStream; +const HttpStream = require("@tryghost/http-stream"); const sandbox = sinon.createSandbox(); -const {Worker} = require('worker_threads'); +const { Worker } = require("worker_threads"); -describe('Logging config', function () { - it('Reads file called loggingrc.js', function () { - const loggerName = 'Logging test'; +describe("Logging config", function () { + it("Reads file called loggingrc.js", function () { + const loggerName = "Logging test"; const loggingRc = `module.exports = { name: "${loggerName}" };`; - fs.writeFileSync('loggingrc.js', loggingRc); + fs.writeFileSync("loggingrc.js", loggingRc); - const ghostLogger = require('../index'); + const ghostLogger = require("../index"); assert.equal(ghostLogger.name, loggerName); - fs.unlinkSync('loggingrc.js'); + fs.unlinkSync("loggingrc.js"); }); - it('Works without loggingrc.js', function () { - const ghostLogger = require('../index'); + it("Works without loggingrc.js", function () { + const ghostLogger = require("../index"); assert.doesNotThrow(() => { - ghostLogger.info('Checking logging works'); + ghostLogger.info("Checking logging works"); }); }); }); -describe('Logging', function () { +describe("Logging", function () { afterEach(function () { sandbox.restore(); }); - it('throws for an invalid transport', function () { + it("throws for an invalid transport", function () { assert.throws(() => { - new GhostLogger({transports: ['nope']}); + new GhostLogger({ transports: ["nope"] }); }, /Nope is an invalid transport/); }); - it('moves stdout to the first transport position', function () { + it("moves stdout to the first transport position", function () { const ghostLogger = new GhostLogger({ - transports: ['stderr', 'stdout'] + transports: ["stderr", "stdout"], }); - assert.equal(ghostLogger.transports[0], 'stdout'); + assert.equal(ghostLogger.transports[0], "stdout"); }); - it('respects LOIN env override for level and mode', function () { - process.env.LOIN = '1'; + it("respects LOIN env override for level and mode", function () { + process.env.LOIN = "1"; try { - const ghostLogger = new GhostLogger({transports: []}); - assert.equal(ghostLogger.level, 'info'); - assert.equal(ghostLogger.mode, 'long'); + const ghostLogger = new GhostLogger({ transports: [] }); + assert.equal(ghostLogger.level, "info"); + assert.equal(ghostLogger.mode, "long"); } finally { delete process.env.LOIN; } @@ -69,131 +69,141 @@ describe('Logging', function () { // in Bunyan 1.8.3 they have changed this behaviour // they are trying to find the err.message attribute and forward this as msg property // our PrettyStream implementation can't handle this case - it('ensure stdout write properties', async function () { + it("ensure stdout write properties", async function () { await new Promise((resolve) => { - sandbox.stub(PrettyStream.prototype, 'write').callsFake(function (data) { + sandbox.stub(PrettyStream.prototype, "write").callsFake(function (data) { assert.notEqual(data.req, null); assert.notEqual(data.req.headers, null); assert.equal(data.req.body, undefined); assert.notEqual(data.res, null); assert.notEqual(data.err, null); - assert.equal(data.name, 'testLogging'); - assert.equal(data.msg, 'message'); + assert.equal(data.name, "testLogging"); + assert.equal(data.msg, "message"); resolve(); }); - var ghostLogger = new GhostLogger({name: 'testLogging'}); - ghostLogger.info({err: new Error('message'), req: {body: {}, headers: {}}, res: {getHeaders: () => ({})}}); + var ghostLogger = new GhostLogger({ name: "testLogging" }); + ghostLogger.info({ + err: new Error("message"), + req: { body: {}, headers: {} }, + res: { getHeaders: () => ({}) }, + }); }); }); - it('ensure stdout write properties with custom message', async function () { + it("ensure stdout write properties with custom message", async function () { await new Promise((resolve) => { - sandbox.stub(PrettyStream.prototype, 'write').callsFake(function (data) { + sandbox.stub(PrettyStream.prototype, "write").callsFake(function (data) { assert.notEqual(data, null); - assert.equal(data.name, 'Log'); - assert.equal(data.msg, 'A handled error! Original message'); + assert.equal(data.name, "Log"); + assert.equal(data.msg, "A handled error! Original message"); resolve(); }); var ghostLogger = new GhostLogger(); - ghostLogger.warn('A handled error!', new Error('Original message')); + ghostLogger.warn("A handled error!", new Error("Original message")); }); }); - it('ensure stdout write properties with object', async function () { + it("ensure stdout write properties with object", async function () { await new Promise((resolve) => { - sandbox.stub(PrettyStream.prototype, 'write').callsFake(function (data) { + sandbox.stub(PrettyStream.prototype, "write").callsFake(function (data) { assert.notEqual(data.err, null); assert.equal(data.test, 2); - assert.equal(data.name, 'Log'); - assert.equal(data.msg, 'Got an error from 3rd party service X! Resource could not be found.'); + assert.equal(data.name, "Log"); + assert.equal( + data.msg, + "Got an error from 3rd party service X! Resource could not be found.", + ); resolve(); }); var ghostLogger = new GhostLogger(); - ghostLogger.error({err: new errors.NotFoundError(), test: 2}, 'Got an error from 3rd party service X!'); + ghostLogger.error( + { err: new errors.NotFoundError(), test: 2 }, + "Got an error from 3rd party service X!", + ); }); }); - it('ensure stdout write metadata properties', async function () { + it("ensure stdout write metadata properties", async function () { await new Promise((resolve) => { - sandbox.stub(PrettyStream.prototype, 'write').callsFake(function (data) { + sandbox.stub(PrettyStream.prototype, "write").callsFake(function (data) { assert.equal(data.version, 2); - assert.equal(data.msg, 'Message to be logged!'); + assert.equal(data.msg, "Message to be logged!"); resolve(); }); - var ghostLogger = new GhostLogger({metadata: {version: 2}}); - ghostLogger.info('Message to be logged!'); + var ghostLogger = new GhostLogger({ metadata: { version: 2 } }); + ghostLogger.info("Message to be logged!"); }); }); - it('ensure stdout write properties with util.format', async function () { + it("ensure stdout write properties with util.format", async function () { await new Promise((resolve) => { - sandbox.stub(PrettyStream.prototype, 'write').callsFake(function (data) { + sandbox.stub(PrettyStream.prototype, "write").callsFake(function (data) { assert.notEqual(data, null); - assert.equal(data.name, 'Log'); - assert.equal(data.msg, 'Message with format'); + assert.equal(data.name, "Log"); + assert.equal(data.msg, "Message with format"); resolve(); }); var ghostLogger = new GhostLogger(); - var thing = 'format'; - ghostLogger.info('Message with %s', thing); + var thing = "format"; + ghostLogger.info("Message with %s", thing); }); }); - it('redact sensitive data with request body', async function () { + it("redact sensitive data with request body", async function () { await new Promise((resolve) => { - sandbox.stub(PrettyStream.prototype, 'write').callsFake(function (data) { + sandbox.stub(PrettyStream.prototype, "write").callsFake(function (data) { assert.notEqual(data.req.body.password, null); - assert.equal(data.req.body.password, '**REDACTED**'); + assert.equal(data.req.body.password, "**REDACTED**"); assert.notEqual(data.req.body.data.attributes.pin, null); - assert.equal(data.req.body.data.attributes.pin, '**REDACTED**'); + assert.equal(data.req.body.data.attributes.pin, "**REDACTED**"); assert.notEqual(data.req.body.data.attributes.test, null); assert.notEqual(data.err, null); assert.notEqual(data.err.errorDetails, null); resolve(); }); - var ghostLogger = new GhostLogger({logBody: true}); + var ghostLogger = new GhostLogger({ logBody: true }); ghostLogger.error({ - err: new errors.IncorrectUsageError({message: 'Hallo', errorDetails: []}), + err: new errors.IncorrectUsageError({ message: "Hallo", errorDetails: [] }), req: { body: { - password: '12345678', + password: "12345678", data: { attributes: { - pin: '1234', - test: 'ja' - } - } + pin: "1234", + test: "ja", + }, + }, }, headers: { - authorization: 'secret', - Connection: 'keep-alive' - } + authorization: "secret", + Connection: "keep-alive", + }, }, - res: {getHeaders: () => ({})} + res: { getHeaders: () => ({}) }, }); }); }); - it('gelf writes a log message', async function () { + it("gelf writes a log message", async function () { await new Promise((resolve) => { - sandbox.stub(GelfStream.prototype, '_write').callsFake(function (data) { + sandbox.stub(GelfStream.prototype, "_write").callsFake(function (data) { assert.notEqual(data.err, null); resolve(); }); var ghostLogger = new GhostLogger({ - transports: ['gelf'], + transports: ["gelf"], gelf: { - host: 'localhost', - port: 12201 - } + host: "localhost", + port: 12201, + }, }); ghostLogger.error(new errors.NotFoundError()); @@ -201,45 +211,45 @@ describe('Logging', function () { }); }); - it('gelf uses default host and port when not provided', function () { + it("gelf uses default host and port when not provided", function () { const ghostLogger = new GhostLogger({ - transports: ['gelf'], - gelf: {} + transports: ["gelf"], + gelf: {}, }); assert.notEqual(ghostLogger.streams.gelf, undefined); }); - it('gelf does not write a log message', function () { - sandbox.spy(GelfStream.prototype, '_write'); + it("gelf does not write a log message", function () { + sandbox.spy(GelfStream.prototype, "_write"); var ghostLogger = new GhostLogger({ - transports: ['gelf'], - level: 'warn', + transports: ["gelf"], + level: "warn", gelf: { - host: 'localhost', - port: 12201 - } + host: "localhost", + port: 12201, + }, }); - ghostLogger.info('testing'); + ghostLogger.info("testing"); assert.equal(GelfStream.prototype._write.called, false); }); - it('loggly does only stream certain errors', async function () { + it("loggly does only stream certain errors", async function () { await new Promise((resolve) => { - sandbox.stub(Bunyan2Loggly.prototype, 'write').callsFake(function (data) { + sandbox.stub(Bunyan2Loggly.prototype, "write").callsFake(function (data) { assert.notEqual(data.err, null); resolve(); }); var ghostLogger = new GhostLogger({ - transports: ['loggly'], + transports: ["loggly"], loggly: { - token: 'invalid', - subdomain: 'invalid', - match: 'level:critical' - } + token: "invalid", + subdomain: "invalid", + match: "level:critical", + }, }); ghostLogger.error(new errors.InternalServerError()); @@ -247,84 +257,84 @@ describe('Logging', function () { }); }); - it('loggly does not stream non-critical errors when matching critical', function () { - sandbox.spy(Bunyan2Loggly.prototype, 'write'); + it("loggly does not stream non-critical errors when matching critical", function () { + sandbox.spy(Bunyan2Loggly.prototype, "write"); var ghostLogger = new GhostLogger({ - transports: ['loggly'], + transports: ["loggly"], loggly: { - token: 'invalid', - subdomain: 'invalid', - match: 'level:critical' - } + token: "invalid", + subdomain: "invalid", + match: "level:critical", + }, }); ghostLogger.error(new errors.NotFoundError()); assert.equal(Bunyan2Loggly.prototype.write.called, false); }); - it('loggly does not stream errors that do not match regex', function () { - sandbox.spy(Bunyan2Loggly.prototype, 'write'); + it("loggly does not stream errors that do not match regex", function () { + sandbox.spy(Bunyan2Loggly.prototype, "write"); var ghostLogger = new GhostLogger({ - transports: ['loggly'], + transports: ["loggly"], loggly: { - token: 'invalid', - subdomain: 'invalid', - match: '^((?!statusCode:4\\d{2}).)*$' - } + token: "invalid", + subdomain: "invalid", + match: "^((?!statusCode:4\\d{2}).)*$", + }, }); ghostLogger.error(new errors.NotFoundError()); assert.equal(Bunyan2Loggly.prototype.write.called, false); }); - it('loggly does not stream errors when not nested correctly', function () { - sandbox.spy(Bunyan2Loggly.prototype, 'write'); + it("loggly does not stream errors when not nested correctly", function () { + sandbox.spy(Bunyan2Loggly.prototype, "write"); var ghostLogger = new GhostLogger({ - transports: ['loggly'], + transports: ["loggly"], loggly: { - token: 'invalid', - subdomain: 'invalid', - match: '^((?!statusCode:4\\d{2}).)*$' - } + token: "invalid", + subdomain: "invalid", + match: "^((?!statusCode:4\\d{2}).)*$", + }, }); ghostLogger.error(new errors.NoPermissionError()); assert.equal(Bunyan2Loggly.prototype.write.called, false); }); - it('loggly does stream errors that match regex', function () { - sandbox.stub(Bunyan2Loggly.prototype, 'write').callsFake(function () {}); + it("loggly does stream errors that match regex", function () { + sandbox.stub(Bunyan2Loggly.prototype, "write").callsFake(function () {}); var ghostLogger = new GhostLogger({ - transports: ['loggly'], + transports: ["loggly"], loggly: { - token: 'invalid', - subdomain: 'invalid', - match: '^((?!statusCode:4\\d{2}).)*$' - } + token: "invalid", + subdomain: "invalid", + match: "^((?!statusCode:4\\d{2}).)*$", + }, }); ghostLogger.error(new errors.InternalServerError()); assert.equal(Bunyan2Loggly.prototype.write.called, true); }); - it('loggly does stream errors that match normal level', async function () { + it("loggly does stream errors that match normal level", async function () { await new Promise((resolve) => { - sandbox.stub(Bunyan2Loggly.prototype, 'write').callsFake(function (data) { + sandbox.stub(Bunyan2Loggly.prototype, "write").callsFake(function (data) { assert.notEqual(data.err, null); resolve(); }); var ghostLogger = new GhostLogger({ - transports: ['loggly'], + transports: ["loggly"], loggly: { - token: 'invalid', - subdomain: 'invalid', - match: 'level:normal' - } + token: "invalid", + subdomain: "invalid", + match: "level:normal", + }, }); ghostLogger.error(new errors.NotFoundError()); @@ -332,36 +342,36 @@ describe('Logging', function () { }); }); - it('loggly match can evaluate with null err payload', function () { - sandbox.stub(Bunyan2Loggly.prototype, 'write').callsFake(function () {}); + it("loggly match can evaluate with null err payload", function () { + sandbox.stub(Bunyan2Loggly.prototype, "write").callsFake(function () {}); const ghostLogger = new GhostLogger({ - transports: ['loggly'], + transports: ["loggly"], loggly: { - token: 'invalid', - subdomain: 'invalid', - match: '^null$' - } + token: "invalid", + subdomain: "invalid", + match: "^null$", + }, }); - ghostLogger.error('plain error message'); + ghostLogger.error("plain error message"); assert.equal(Bunyan2Loggly.prototype.write.called, true); }); - it('loggly does stream errors that match an one of multiple match statements', async function () { + it("loggly does stream errors that match an one of multiple match statements", async function () { await new Promise((resolve) => { - sandbox.stub(Bunyan2Loggly.prototype, 'write').callsFake(function (data) { + sandbox.stub(Bunyan2Loggly.prototype, "write").callsFake(function (data) { assert.notEqual(data.err, null); resolve(); }); var ghostLogger = new GhostLogger({ - transports: ['loggly'], + transports: ["loggly"], loggly: { - token: 'invalid', - subdomain: 'invalid', - match: 'level:critical|statusCode:404' - } + token: "invalid", + subdomain: "invalid", + match: "level:critical|statusCode:404", + }, }); ghostLogger.error(new errors.NotFoundError()); @@ -369,9 +379,9 @@ describe('Logging', function () { }); }); - it('loggly does stream errors that match status code: full example', async function () { + it("loggly does stream errors that match status code: full example", async function () { await new Promise((resolve) => { - sandbox.stub(Bunyan2Loggly.prototype, 'write').callsFake(function (data) { + sandbox.stub(Bunyan2Loggly.prototype, "write").callsFake(function (data) { assert.notEqual(data.err, null); assert.notEqual(data.req, null); assert.notEqual(data.res, null); @@ -379,36 +389,41 @@ describe('Logging', function () { }); var ghostLogger = new GhostLogger({ - transports: ['loggly'], + transports: ["loggly"], loggly: { - token: 'invalid', - subdomain: 'invalid', - match: 'statusCode:404' - } + token: "invalid", + subdomain: "invalid", + match: "statusCode:404", + }, }); ghostLogger.error({ err: new errors.NotFoundError(), - req: {body: {password: '12345678', data: {attributes: {pin: '1234', test: 'ja'}}}}, - res: {getHeaders: () => ({})} + req: { + body: { + password: "12345678", + data: { attributes: { pin: "1234", test: "ja" } }, + }, + }, + res: { getHeaders: () => ({}) }, }); assert.equal(Bunyan2Loggly.prototype.write.called, true); }); }); - it('loggly does only stream certain errors: match is not defined -> log everything', async function () { + it("loggly does only stream certain errors: match is not defined -> log everything", async function () { await new Promise((resolve) => { - sandbox.stub(Bunyan2Loggly.prototype, 'write').callsFake(function (data) { + sandbox.stub(Bunyan2Loggly.prototype, "write").callsFake(function (data) { assert.notEqual(data.err, null); resolve(); }); var ghostLogger = new GhostLogger({ - transports: ['loggly'], + transports: ["loggly"], loggly: { - token: 'invalid', - subdomain: 'invalid' - } + token: "invalid", + subdomain: "invalid", + }, }); ghostLogger.error(new errors.NotFoundError()); @@ -416,56 +431,64 @@ describe('Logging', function () { }); }); - it('elasticsearch should make a stream', function () { - const es = new ElasticSearch({ - node: 'http://test-elastic-client', - auth: { - username: 'user', - password: 'pass' - } - }, 'index', 'pipeline'); + it("elasticsearch should make a stream", function () { + const es = new ElasticSearch( + { + node: "http://test-elastic-client", + auth: { + username: "user", + password: "pass", + }, + }, + "index", + "pipeline", + ); const stream = es.getStream(); - assert.equal(typeof stream.write, 'function'); + assert.equal(typeof stream.write, "function"); }); - it('elasticsearch should receive a single object', async function () { - sandbox.stub(ElasticSearch.prototype, 'getStream').returns({ + it("elasticsearch should receive a single object", async function () { + sandbox.stub(ElasticSearch.prototype, "getStream").returns({ write: function (jsonData) { assert.equal(arguments.length, 1); const data = JSON.parse(jsonData); - assert.equal(data.msg, 'hello 1'); - assert.equal(data.prop, 'prop val'); - } + assert.equal(data.msg, "hello 1"); + assert.equal(data.prop, "prop val"); + }, }); var ghostLogger = new GhostLogger({ - transports: ['elasticsearch'], + transports: ["elasticsearch"], elasticsearch: { - index: 'ghost-index', - username: 'example', - password: 'password', - host: 'elastic-search-host' - } + index: "ghost-index", + username: "example", + password: "password", + host: "elastic-search-host", + }, }); - await ghostLogger.info({ - prop: 'prop val' - }, 'hello', 1); + await ghostLogger.info( + { + prop: "prop val", + }, + "hello", + 1, + ); }); - it('http writes a log message', async function () { + it("http writes a log message", async function () { await new Promise((resolve) => { - sandbox.stub(HttpStream.prototype, 'write').callsFake(function (data) { + sandbox.stub(HttpStream.prototype, "write").callsFake(function (data) { assert.notEqual(data.err, null); resolve(); }); var ghostLogger = new GhostLogger({ - transports: ['http'], + transports: ["http"], http: { - host: 'http://localhost' - } + host: "http://localhost", + }, }); ghostLogger.error(new errors.NotFoundError()); @@ -473,128 +496,145 @@ describe('Logging', function () { }); }); - it('http does not write an info log in error mode', function () { - sandbox.spy(HttpStream.prototype, 'write'); + it("http does not write an info log in error mode", function () { + sandbox.spy(HttpStream.prototype, "write"); var ghostLogger = new GhostLogger({ - transports: ['http'], + transports: ["http"], http: { - host: 'http://localhost', - level: 'error' - } + host: "http://localhost", + level: "error", + }, }); - ghostLogger.info('testing'); + ghostLogger.info("testing"); assert.equal(HttpStream.prototype.write.called, false); }); - it('http can write errors in info mode', function () { - sandbox.spy(HttpStream.prototype, 'write'); + it("http can write errors in info mode", function () { + sandbox.spy(HttpStream.prototype, "write"); var ghostLogger = new GhostLogger({ - transports: ['http'], + transports: ["http"], http: { - host: 'http://localhost', - level: 'info' - } + host: "http://localhost", + level: "info", + }, }); - ghostLogger.error('testing'); + ghostLogger.error("testing"); assert.equal(HttpStream.prototype.write.called, true); }); - it('automatically adds stdout to transports if stderr transport is configured and stdout isn\'t', function () { + it("automatically adds stdout to transports if stderr transport is configured and stdout isn't", function () { var ghostLogger = new GhostLogger({ - transports: ['stderr'] + transports: ["stderr"], }); - assert.equal(includes(ghostLogger.transports, 'stderr'), true, 'stderr transport should exist'); - assert.equal(includes(ghostLogger.transports, 'stdout'), true, 'stdout transport should exist'); + assert.equal( + includes(ghostLogger.transports, "stderr"), + true, + "stderr transport should exist", + ); + assert.equal( + includes(ghostLogger.transports, "stdout"), + true, + "stdout transport should exist", + ); }); - it('logs errors only to stderr if both stdout and stderr transports are defined', function () { - var stderr = sandbox.spy(process.stderr, 'write'); - var stdout = sandbox.spy(process.stdout, 'write'); + it("logs errors only to stderr if both stdout and stderr transports are defined", function () { + var stderr = sandbox.spy(process.stderr, "write"); + var stdout = sandbox.spy(process.stdout, "write"); var ghostLogger = new GhostLogger({ - transports: ['stdout', 'stderr'] + transports: ["stdout", "stderr"], }); - ghostLogger.error('some error'); + ghostLogger.error("some error"); assert.equal(stderr.calledOnce, true); - assert.equal(stdout.called, false, 'stdout should not be written to'); + assert.equal(stdout.called, false, "stdout should not be written to"); }); - it('logs to parent port when in a worker thread', async function () { - const worker = new Worker('./test/fixtures/worker.js'); + it("logs to parent port when in a worker thread", async function () { + const worker = new Worker("./test/fixtures/worker.js"); try { const data = await new Promise((resolve, reject) => { - worker.once('message', resolve); - worker.once('error', reject); - worker.once('exit', (code) => { + worker.once("message", resolve); + worker.once("error", reject); + worker.once("exit", (code) => { if (code !== 0) { reject(new Error(`Worker exited with code ${code}`)); } }); }); - assert.equal(data, 'Hello!'); + assert.equal(data, "Hello!"); } finally { await worker.terminate(); } }); - describe('filename computation', function () { - it('sanitizeDomain should replace non-word characters with underscores', function () { + describe("filename computation", function () { + it("sanitizeDomain should replace non-word characters with underscores", function () { var ghostLogger = new GhostLogger(); - assert.equal(ghostLogger.sanitizeDomain('http://my-domain.com'), 'http___my_domain_com'); - assert.equal(ghostLogger.sanitizeDomain('localhost'), 'localhost'); - assert.equal(ghostLogger.sanitizeDomain('example.com:8080'), 'example_com_8080'); + assert.equal( + ghostLogger.sanitizeDomain("http://my-domain.com"), + "http___my_domain_com", + ); + assert.equal(ghostLogger.sanitizeDomain("localhost"), "localhost"); + assert.equal(ghostLogger.sanitizeDomain("example.com:8080"), "example_com_8080"); }); - it('replaceFilenamePlaceholders should replace {env} placeholder', function () { - var ghostLogger = new GhostLogger({env: 'production'}); - assert.equal(ghostLogger.replaceFilenamePlaceholders('{env}'), 'production'); + it("replaceFilenamePlaceholders should replace {env} placeholder", function () { + var ghostLogger = new GhostLogger({ env: "production" }); + assert.equal(ghostLogger.replaceFilenamePlaceholders("{env}"), "production"); }); - it('replaceFilenamePlaceholders should replace {domain} placeholder', function () { - var ghostLogger = new GhostLogger({domain: 'http://example.com'}); - assert.equal(ghostLogger.replaceFilenamePlaceholders('{domain}'), 'http___example_com'); + it("replaceFilenamePlaceholders should replace {domain} placeholder", function () { + var ghostLogger = new GhostLogger({ domain: "http://example.com" }); + assert.equal(ghostLogger.replaceFilenamePlaceholders("{domain}"), "http___example_com"); }); - it('replaceFilenamePlaceholders should replace both {env} and {domain} placeholders', function () { + it("replaceFilenamePlaceholders should replace both {env} and {domain} placeholders", function () { var ghostLogger = new GhostLogger({ - domain: 'http://example.com', - env: 'staging' + domain: "http://example.com", + env: "staging", }); - assert.equal(ghostLogger.replaceFilenamePlaceholders('{domain}-{env}'), 'http___example_com-staging'); - assert.equal(ghostLogger.replaceFilenamePlaceholders('{env}.{domain}'), 'staging.http___example_com'); + assert.equal( + ghostLogger.replaceFilenamePlaceholders("{domain}-{env}"), + "http___example_com-staging", + ); + assert.equal( + ghostLogger.replaceFilenamePlaceholders("{env}.{domain}"), + "staging.http___example_com", + ); }); - it('logger should return default format when no filename option provided', function () { + it("logger should return default format when no filename option provided", function () { var ghostLogger = new GhostLogger({ - domain: 'http://example.com', - env: 'production' + domain: "http://example.com", + env: "production", }); - assert.equal(ghostLogger.filename, '{domain}_{env}'); + assert.equal(ghostLogger.filename, "{domain}_{env}"); }); - it('logger should use filename template when provided', function () { + it("logger should use filename template when provided", function () { var ghostLogger = new GhostLogger({ - domain: 'http://example.com', - env: 'production', - filename: '{env}' + domain: "http://example.com", + env: "production", + filename: "{env}", }); - assert.equal(ghostLogger.filename, '{env}'); + assert.equal(ghostLogger.filename, "{env}"); }); - it('file stream should use custom filename template', function () { - const tempDir = './test-logs/'; + it("file stream should use custom filename template", function () { + const tempDir = "./test-logs/"; const rimraf = function (dir) { if (fs.existsSync(dir)) { fs.readdirSync(dir).forEach(function (file) { - const curPath = dir + '/' + file; + const curPath = dir + "/" + file; if (fs.lstatSync(curPath).isDirectory()) { rimraf(curPath); } else { @@ -607,124 +647,124 @@ describe('Logging', function () { // Create temp directory if (!fs.existsSync(tempDir)) { - fs.mkdirSync(tempDir, {recursive: true}); + fs.mkdirSync(tempDir, { recursive: true }); } var ghostLogger = new GhostLogger({ - domain: 'test.com', - env: 'production', - filename: '{env}', - transports: ['file'], - path: tempDir + domain: "test.com", + env: "production", + filename: "{env}", + transports: ["file"], + path: tempDir, }); - ghostLogger.info('Test log message'); + ghostLogger.info("Test log message"); // Give it a moment to write setTimeout(function () { - assert.equal(fs.existsSync(tempDir + 'production.log'), true); - assert.equal(fs.existsSync(tempDir + 'production.error.log'), true); + assert.equal(fs.existsSync(tempDir + "production.log"), true); + assert.equal(fs.existsSync(tempDir + "production.error.log"), true); // Cleanup rimraf(tempDir); }, 100); }); - it('file stream supports built-in rotating-file transport config', function () { - const tempDir = './test-logs-rotation-built-in/'; + it("file stream supports built-in rotating-file transport config", function () { + const tempDir = "./test-logs-rotation-built-in/"; if (!fs.existsSync(tempDir)) { - fs.mkdirSync(tempDir, {recursive: true}); + fs.mkdirSync(tempDir, { recursive: true }); } const ghostLogger = new GhostLogger({ - domain: 'test.com', - env: 'production', - transports: ['file'], + domain: "test.com", + env: "production", + transports: ["file"], path: tempDir, rotation: { enabled: true, useLibrary: false, - period: '1d', - count: 2 - } + period: "1d", + count: 2, + }, }); - assert.notEqual(ghostLogger.streams['rotation-errors'], undefined); - assert.notEqual(ghostLogger.streams['rotation-all'], undefined); + assert.notEqual(ghostLogger.streams["rotation-errors"], undefined); + assert.notEqual(ghostLogger.streams["rotation-all"], undefined); }); - it('file stream supports external rotating file library config', function () { - const tempDir = './test-logs-rotation-library/'; + it("file stream supports external rotating file library config", function () { + const tempDir = "./test-logs-rotation-library/"; if (!fs.existsSync(tempDir)) { - fs.mkdirSync(tempDir, {recursive: true}); + fs.mkdirSync(tempDir, { recursive: true }); } const ghostLogger = new GhostLogger({ - domain: 'test.com', - env: 'production', - transports: ['file'], + domain: "test.com", + env: "production", + transports: ["file"], path: tempDir, rotation: { enabled: true, useLibrary: true, - period: '1d', - threshold: '10m', + period: "1d", + threshold: "10m", count: 2, gzip: true, - rotateExisting: false - } + rotateExisting: false, + }, }); - assert.notEqual(ghostLogger.streams['rotation-errors'], undefined); - assert.notEqual(ghostLogger.streams['rotation-all'], undefined); + assert.notEqual(ghostLogger.streams["rotation-errors"], undefined); + assert.notEqual(ghostLogger.streams["rotation-all"], undefined); }); - it('file stream rotation library handles missing rotateExisting option', function () { - const tempDir = './test-logs-rotation-library-default/'; + it("file stream rotation library handles missing rotateExisting option", function () { + const tempDir = "./test-logs-rotation-library-default/"; if (!fs.existsSync(tempDir)) { - fs.mkdirSync(tempDir, {recursive: true}); + fs.mkdirSync(tempDir, { recursive: true }); } const ghostLogger = new GhostLogger({ - domain: 'test.com', - env: 'production', - transports: ['file'], + domain: "test.com", + env: "production", + transports: ["file"], path: tempDir, rotation: { enabled: true, useLibrary: true, - period: '1d', - threshold: '10m', + period: "1d", + threshold: "10m", count: 2, - gzip: true - } + gzip: true, + }, }); - assert.notEqual(ghostLogger.streams['rotation-errors'], undefined); - assert.notEqual(ghostLogger.streams['rotation-all'], undefined); + assert.notEqual(ghostLogger.streams["rotation-errors"], undefined); + assert.notEqual(ghostLogger.streams["rotation-all"], undefined); }); - it('file stream exits early when target directory does not exist', function () { - const badPath = './test-logs-missing-dir/'; + it("file stream exits early when target directory does not exist", function () { + const badPath = "./test-logs-missing-dir/"; const ghostLogger = new GhostLogger({ - transports: ['file'], - path: badPath + transports: ["file"], + path: badPath, }); - assert.equal(ghostLogger.streams['file-errors'], undefined); - assert.equal(ghostLogger.streams['file-all'], undefined); + assert.equal(ghostLogger.streams["file-errors"], undefined); + assert.equal(ghostLogger.streams["file-all"], undefined); }); }); - describe('serialization', function () { - it('serializes error into correct object', async function () { + describe("serialization", function () { + it("serializes error into correct object", async function () { await new Promise((resolve) => { const err = new errors.NotFoundError(); - sandbox.stub(Bunyan2Loggly.prototype, 'write').callsFake(function (data) { + sandbox.stub(Bunyan2Loggly.prototype, "write").callsFake(function (data) { assert.notEqual(data.err, null); assert.equal(data.err.id, err.id); - assert.equal(data.err.domain, 'localhost'); + assert.equal(data.err.domain, "localhost"); assert.equal(data.err.code, null); assert.equal(data.err.name, err.errorType); assert.equal(data.err.statusCode, err.statusCode); @@ -739,23 +779,23 @@ describe('Logging', function () { }); const ghostLogger = new GhostLogger({ - transports: ['loggly'], + transports: ["loggly"], loggly: { - token: 'invalid', - subdomain: 'invalid' - } + token: "invalid", + subdomain: "invalid", + }, }); ghostLogger.error({ - err + err, }); assert.equal(Bunyan2Loggly.prototype.write.called, true); }); }); - it('stringifies meta properties', async function () { + it("stringifies meta properties", async function () { await new Promise((resolve) => { - sandbox.stub(Bunyan2Loggly.prototype, 'write').callsFake(function (data) { + sandbox.stub(Bunyan2Loggly.prototype, "write").callsFake(function (data) { assert.notEqual(data.err, null); assert.equal(data.err.context, '{"a":"b"}'); assert.equal(data.err.errorDetails, '{"c":"d"}'); @@ -764,125 +804,125 @@ describe('Logging', function () { }); const ghostLogger = new GhostLogger({ - transports: ['loggly'], + transports: ["loggly"], loggly: { - token: 'invalid', - subdomain: 'invalid' - } + token: "invalid", + subdomain: "invalid", + }, }); ghostLogger.error({ err: new errors.NotFoundError({ context: { - a: 'b' + a: "b", }, errorDetails: { - c: 'd' + c: "d", }, help: { - b: 'a' - } - }) + b: "a", + }, + }), }); assert.equal(Bunyan2Loggly.prototype.write.called, true); }); }); - it('serializes req extra and queueDepth fields when present', function () { + it("serializes req extra and queueDepth fields when present", function () { const ghostLogger = new GhostLogger(); const req = { - requestId: 'req-1', - userId: 'user-1', - url: '/x', - method: 'GET', - originalUrl: '/x?y=1', + requestId: "req-1", + userId: "user-1", + url: "/x", + method: "GET", + originalUrl: "/x?y=1", params: {}, headers: {}, query: {}, - extra: {feature: 'on'}, - queueDepth: 7 + extra: { feature: "on" }, + queueDepth: 7, }; const serialized = ghostLogger.serializers.req(req); - assert.deepEqual(serialized.extra, {feature: 'on'}); + assert.deepEqual(serialized.extra, { feature: "on" }); assert.equal(serialized.queueDepth, 7); }); - it('removeSensitiveData falls back to original value when recursive sanitization throws', function () { + it("removeSensitiveData falls back to original value when recursive sanitization throws", function () { const ghostLogger = new GhostLogger(); const nested = {}; - Object.defineProperty(nested, 'boom', { + Object.defineProperty(nested, "boom", { enumerable: true, get() { - throw new Error('boom'); - } + throw new Error("boom"); + }, }); - const data = ghostLogger.removeSensitiveData({nested}); + const data = ghostLogger.removeSensitiveData({ nested }); assert.equal(data.nested, nested); }); }); - describe('logger internals', function () { - it('adds local timestamp when useLocalTime is enabled', function () { + describe("logger internals", function () { + it("adds local timestamp when useLocalTime is enabled", function () { const ghostLogger = new GhostLogger({ transports: [], - useLocalTime: true + useLocalTime: true, }); const info = sinon.spy(); ghostLogger.streams = { stdout: { - name: 'stdout', - log: {info} - } + name: "stdout", + log: { info }, + }, }; - ghostLogger.log('info', ['hello']); + ghostLogger.log("info", ["hello"]); assert.equal(info.calledOnce, true); assert.notEqual(info.args[0][0].time, undefined); }); - it('trace/debug/fatal delegate to log()', function () { - const ghostLogger = new GhostLogger({transports: []}); - const logSpy = sinon.spy(ghostLogger, 'log'); + it("trace/debug/fatal delegate to log()", function () { + const ghostLogger = new GhostLogger({ transports: [] }); + const logSpy = sinon.spy(ghostLogger, "log"); - ghostLogger.trace('t'); - ghostLogger.debug('d'); - ghostLogger.fatal('f'); + ghostLogger.trace("t"); + ghostLogger.debug("d"); + ghostLogger.fatal("f"); - assert.equal(logSpy.args[0][0], 'trace'); - assert.equal(logSpy.args[1][0], 'debug'); - assert.equal(logSpy.args[2][0], 'fatal'); + assert.equal(logSpy.args[0][0], "trace"); + assert.equal(logSpy.args[1][0], "debug"); + assert.equal(logSpy.args[2][0], "fatal"); }); - it('child creates stream children with bound properties', function () { - const ghostLogger = new GhostLogger({transports: []}); - const childA = sinon.stub().returns({id: 'a'}); - const childB = sinon.stub().returns({id: 'b'}); + it("child creates stream children with bound properties", function () { + const ghostLogger = new GhostLogger({ transports: [] }); + const childA = sinon.stub().returns({ id: "a" }); + const childB = sinon.stub().returns({ id: "b" }); ghostLogger.streams = { one: { - name: 'one', - log: {child: childA} + name: "one", + log: { child: childA }, }, two: { - name: 'two', - log: {child: childB} - } + name: "two", + log: { child: childB }, + }, }; - const child = ghostLogger.child({requestId: 'abc'}); + const child = ghostLogger.child({ requestId: "abc" }); assert.equal(childA.calledOnce, true); assert.equal(childB.calledOnce, true); - assert.deepEqual(childA.args[0][0], {requestId: 'abc'}); - assert.deepEqual(childB.args[0][0], {requestId: 'abc'}); - assert.equal(child.streams.one.name, 'one'); - assert.equal(child.streams.two.name, 'two'); - assert.deepEqual(child.streams.one.log, {id: 'a'}); - assert.deepEqual(child.streams.two.log, {id: 'b'}); + assert.deepEqual(childA.args[0][0], { requestId: "abc" }); + assert.deepEqual(childB.args[0][0], { requestId: "abc" }); + assert.equal(child.streams.one.name, "one"); + assert.equal(child.streams.two.name, "two"); + assert.deepEqual(child.streams.one.log, { id: "a" }); + assert.deepEqual(child.streams.two.log, { id: "b" }); }); }); }); diff --git a/packages/metrics/README.md b/packages/metrics/README.md index a1b46def1..fb31d6a33 100644 --- a/packages/metrics/README.md +++ b/packages/metrics/README.md @@ -8,36 +8,30 @@ or `yarn add @tryghost/metrics` - ## Purpose Ghost metrics facade for collecting and emitting operational metrics across services. ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests +# Copyright & License - - -# Copyright & License - -Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/metrics/index.js b/packages/metrics/index.js index 72e821156..c9290c730 100644 --- a/packages/metrics/index.js +++ b/packages/metrics/index.js @@ -1 +1 @@ -module.exports = require('./lib/metrics'); +module.exports = require("./lib/metrics"); diff --git a/packages/metrics/lib/GhostMetrics.js b/packages/metrics/lib/GhostMetrics.js index 0c5f0732c..893fe86f9 100644 --- a/packages/metrics/lib/GhostMetrics.js +++ b/packages/metrics/lib/GhostMetrics.js @@ -1,4 +1,4 @@ -const jsonStringifySafe = require('json-stringify-safe'); +const jsonStringifySafe = require("json-stringify-safe"); /** * @description Metric shipper class built on the loggingrc config used in Ghost projects @@ -17,10 +17,10 @@ class GhostMetrics { constructor(options) { options = options || {}; - this.domain = options.domain || 'localhost'; + this.domain = options.domain || "localhost"; this.elasticsearch = options.elasticsearch || {}; - this.mode = process.env.MODE || options.mode || 'short'; - if ('metrics' in options && typeof options.metrics === 'object') { + this.mode = process.env.MODE || options.mode || "short"; + if ("metrics" in options && typeof options.metrics === "object") { this.transports = options.metrics.transports || []; this.metadata = options.metrics.metadata || {}; } else { @@ -30,7 +30,7 @@ class GhostMetrics { // CASE: special env variable to enable long mode and level info if (process.env.LOIN) { - this.mode = 'long'; + this.mode = "long"; } this.shippers = {}; @@ -50,9 +50,9 @@ class GhostMetrics { * @description Setup stdout stream. */ setupStdoutShipper() { - const GhostPrettyStream = require('@tryghost/pretty-stream'); + const GhostPrettyStream = require("@tryghost/pretty-stream"); const prettyStdOut = new GhostPrettyStream({ - mode: this.mode + mode: this.mode, }); prettyStdOut.pipe(process.stdout); @@ -60,7 +60,7 @@ class GhostMetrics { this.shippers.stdout = (name, value) => { prettyStdOut.write({ msg: `Metric ${name}: ${jsonStringifySafe(value)}`, - level: 30 // Magic number, log level for info + level: 30, // Magic number, log level for info }); return Promise.resolve(); @@ -73,25 +73,25 @@ class GhostMetrics { * The name of the index is the name of the metric prefixed with "metrics-", the metric name itself should be sluggified */ setupElasticsearchShipper() { - const ElasticSearch = require('@tryghost/elasticsearch'); + const ElasticSearch = require("@tryghost/elasticsearch"); const elasticSearch = new ElasticSearch({ node: this.elasticsearch.host, auth: { username: this.elasticsearch.username, - password: this.elasticsearch.password + password: this.elasticsearch.password, }, requestTimeout: 5000, - proxy: 'proxy' in this.elasticsearch ? this.elasticsearch.proxy : null + proxy: "proxy" in this.elasticsearch ? this.elasticsearch.proxy : null, }); this.shippers.elasticsearch = (name, value) => { - if (typeof value !== 'object') { - value = {value}; + if (typeof value !== "object") { + value = { value }; } - if (!('@timestamp' in value)) { - value['@timestamp'] = Date.now(); + if (!("@timestamp" in value)) { + value["@timestamp"] = Date.now(); } if (this.metadata) { diff --git a/packages/metrics/lib/metrics.js b/packages/metrics/lib/metrics.js index 374edd50f..f73f05a75 100644 --- a/packages/metrics/lib/metrics.js +++ b/packages/metrics/lib/metrics.js @@ -1,11 +1,11 @@ -const path = require('path'); -const {getProcessRoot} = require('@tryghost/root-utils'); -const GhostMetrics = require('./GhostMetrics'); +const path = require("path"); +const { getProcessRoot } = require("@tryghost/root-utils"); +const GhostMetrics = require("./GhostMetrics"); // Metrics piggy-backs on logging config for transport configuration let loggingConfig; try { - loggingConfig = require(path.join(getProcessRoot(), 'loggingrc')); + loggingConfig = require(path.join(getProcessRoot(), "loggingrc")); } catch (err) { loggingConfig = {}; } diff --git a/packages/metrics/package.json b/packages/metrics/package.json index 5857bcf1a..163538f41 100644 --- a/packages/metrics/package.json +++ b/packages/metrics/package.json @@ -1,36 +1,36 @@ { "name": "@tryghost/metrics", "version": "3.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/metrics" }, - "author": "Ghost Foundation", - "license": "MIT", - "main": "index.js", - "scripts": { - "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing vitest run --coverage", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" - }, "files": [ "index.js", "lib" ], + "main": "index.js", "publishConfig": { "access": "public" }, - "devDependencies": { - "rewire": "9.0.1", - "sinon": "21.0.3" + "scripts": { + "dev": "echo \"Implement me!\"", + "test": "NODE_ENV=testing vitest run --coverage", + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "dependencies": { "@tryghost/elasticsearch": "^5.0.3", "@tryghost/pretty-stream": "^2.0.3", "@tryghost/root-utils": "^2.0.3", "json-stringify-safe": "5.0.1" + }, + "devDependencies": { + "rewire": "9.0.1", + "sinon": "21.0.3" } } diff --git a/packages/metrics/test/.eslintrc.js b/packages/metrics/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/metrics/test/.eslintrc.js +++ b/packages/metrics/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/metrics/test/metrics.test.js b/packages/metrics/test/metrics.test.js index f8016b59a..5d72cddf3 100644 --- a/packages/metrics/test/metrics.test.js +++ b/packages/metrics/test/metrics.test.js @@ -1,11 +1,11 @@ -const fs = require('fs'); -const path = require('path'); -const sinon = require('sinon'); -const assert = require('assert/strict'); -const ElasticSearch = require('@tryghost/elasticsearch'); -const PrettyStream = require('@tryghost/pretty-stream'); -const {getProcessRoot} = require('@tryghost/root-utils'); -const GhostMetrics = require('../lib/GhostMetrics'); +const fs = require("fs"); +const path = require("path"); +const sinon = require("sinon"); +const assert = require("assert/strict"); +const ElasticSearch = require("@tryghost/elasticsearch"); +const PrettyStream = require("@tryghost/pretty-stream"); +const { getProcessRoot } = require("@tryghost/root-utils"); +const GhostMetrics = require("../lib/GhostMetrics"); const sandbox = sinon.createSandbox(); // Vitest sets process.env.MODE to 'test' which interferes with GhostMetrics mode detection @@ -21,55 +21,55 @@ afterEach(function () { } }); -const loggingConfigPath = path.join(getProcessRoot(), 'loggingrc'); +const loggingConfigPath = path.join(getProcessRoot(), "loggingrc"); -describe('Metrics config', function () { +describe("Metrics config", function () { afterEach(function () { - delete require.cache[require.resolve('../lib/metrics')]; - delete require.cache[require.resolve('../index')]; + delete require.cache[require.resolve("../lib/metrics")]; + delete require.cache[require.resolve("../index")]; delete require.cache[loggingConfigPath]; delete require.cache[`${loggingConfigPath}.js`]; }); - it('Reads file called loggingrc.js', function () { - const transports = ['stdout']; + it("Reads file called loggingrc.js", function () { + const transports = ["stdout"]; const loggingRc = `module.exports = { metrics: { - transports: [${transports.map(t => `'${t}'`).join(', ')}] + transports: [${transports.map((t) => `'${t}'`).join(", ")}] } };`; - fs.writeFileSync('loggingrc.js', loggingRc); + fs.writeFileSync("loggingrc.js", loggingRc); - const ghostMetrics = require('../index'); + const ghostMetrics = require("../index"); assert.deepEqual(ghostMetrics.transports, transports); - fs.unlinkSync('loggingrc.js'); + fs.unlinkSync("loggingrc.js"); }); - it('loads with empty config when loggingrc.js is missing', function () { - if (fs.existsSync('loggingrc.js')) { - fs.unlinkSync('loggingrc.js'); + it("loads with empty config when loggingrc.js is missing", function () { + if (fs.existsSync("loggingrc.js")) { + fs.unlinkSync("loggingrc.js"); } delete require.cache[loggingConfigPath]; delete require.cache[`${loggingConfigPath}.js`]; - const ghostMetrics = require('../lib/metrics'); + const ghostMetrics = require("../lib/metrics"); assert.deepEqual(ghostMetrics.transports, []); }); }); -describe('Logging', function () { +describe("Logging", function () { afterEach(function () { sandbox.restore(); }); - it('stdout transport works', async function () { - const name = 'test-metric'; + it("stdout transport works", async function () { + const name = "test-metric"; const value = 101; await new Promise((resolve) => { - sandbox.stub(PrettyStream.prototype, 'write').callsFake(function (data) { + sandbox.stub(PrettyStream.prototype, "write").callsFake(function (data) { assert.notEqual(data.msg, undefined); assert.equal(data.msg, `Metric ${name}: ${JSON.stringify(value)}`); resolve(); @@ -77,40 +77,40 @@ describe('Logging', function () { const ghostMetrics = new GhostMetrics({ metrics: { - transports: ['stdout'] - } + transports: ["stdout"], + }, }); ghostMetrics.metric(name, value); }); }); - it('elasticsearch transport works', async function () { - const name = 'test-metric'; + it("elasticsearch transport works", async function () { + const name = "test-metric"; const value = 101; const ghostMetrics = new GhostMetrics({ metrics: { - transports: ['elasticsearch'], + transports: ["elasticsearch"], metadata: { - id: '123123' - } + id: "123123", + }, }, elasticsearch: { - host: 'https://test-elasticsearch', - username: 'user', - password: 'pass', - level: 'info' - } + host: "https://test-elasticsearch", + username: "user", + password: "pass", + level: "info", + }, }); await new Promise((resolve) => { - sandbox.stub(ElasticSearch.prototype, 'index').callsFake(function (data, index) { + sandbox.stub(ElasticSearch.prototype, "index").callsFake(function (data, index) { assert.notEqual(data.metadata, undefined); assert.equal(data.metadata.id, ghostMetrics.metadata.id); assert.equal(data.value, value); // ElasticSearch shipper prefixes metric names to avoid polluting index namespace - assert.equal(index, 'metrics-' + name); + assert.equal(index, "metrics-" + name); resolve(); }); @@ -118,84 +118,84 @@ describe('Logging', function () { }); }); - it('throws for invalid transport', function () { + it("throws for invalid transport", function () { assert.throws(() => { new GhostMetrics({ metrics: { - transports: ['not-a-transport'] - } + transports: ["not-a-transport"], + }, }); }); }); - it('defaults to short mode', function () { + it("defaults to short mode", function () { const ghostMetrics = new GhostMetrics({ metrics: { - transports: ['stdout'] - } + transports: ["stdout"], + }, }); - assert.equal(ghostMetrics.mode, 'short'); + assert.equal(ghostMetrics.mode, "short"); }); - it('uses long mode when LOIN variable set', function () { - process.env.LOIN = 'set'; + it("uses long mode when LOIN variable set", function () { + process.env.LOIN = "set"; const ghostMetrics = new GhostMetrics({}); - assert.equal(ghostMetrics.mode, 'long'); + assert.equal(ghostMetrics.mode, "long"); delete process.env.LOIN; }); - it('defaults options bag and metrics transport values', function () { + it("defaults options bag and metrics transport values", function () { const noOptionsMetrics = new GhostMetrics(); assert.deepEqual(noOptionsMetrics.transports, []); - const emptyMetricsConfig = new GhostMetrics({metrics: {}}); + const emptyMetricsConfig = new GhostMetrics({ metrics: {} }); assert.deepEqual(emptyMetricsConfig.transports, []); assert.deepEqual(emptyMetricsConfig.metadata, {}); }); - it('resolves even when transport throws', async function () { - const name = 'test-metric'; + it("resolves even when transport throws", async function () { + const name = "test-metric"; const value = 101; const ghostMetrics = new GhostMetrics({ metrics: { - transports: ['elasticsearch'], + transports: ["elasticsearch"], metadata: { - id: '123123' - } + id: "123123", + }, }, elasticsearch: { - host: 'https://test-elasticsearch', - username: 'user', - password: 'pass', - level: 'info' - } + host: "https://test-elasticsearch", + username: "user", + password: "pass", + level: "info", + }, }); - sandbox.stub(ElasticSearch.prototype, 'index').rejects(); + sandbox.stub(ElasticSearch.prototype, "index").rejects(); await assert.doesNotReject(() => ghostMetrics.metric(name, value)); }); - it('passes configured proxy to elasticsearch', function () { - const name = 'proxy-metric'; + it("passes configured proxy to elasticsearch", function () { + const name = "proxy-metric"; const value = 2; const ghostMetrics = new GhostMetrics({ metrics: { - transports: ['elasticsearch'] + transports: ["elasticsearch"], }, elasticsearch: { - host: 'https://test-elasticsearch', - username: 'user', - password: 'pass', - proxy: 'https://proxy.example.com' - } + host: "https://test-elasticsearch", + username: "user", + password: "pass", + proxy: "https://proxy.example.com", + }, }); - sandbox.stub(ElasticSearch.prototype, 'index').resolves(); + sandbox.stub(ElasticSearch.prototype, "index").resolves(); ghostMetrics.metric(name, value); assert.equal(ElasticSearch.prototype.index.calledOnce, true); }); diff --git a/packages/mw-error-handler/README.md b/packages/mw-error-handler/README.md index b7321f557..7a563d98b 100644 --- a/packages/mw-error-handler/README.md +++ b/packages/mw-error-handler/README.md @@ -17,14 +17,14 @@ or ## Usage ```js -const express = require('express'); -const sentry = require('./sentry'); -const errorHandler = require('@tryghost/mw-error-handler'); +const express = require("express"); +const sentry = require("./sentry"); +const errorHandler = require("@tryghost/mw-error-handler"); const app = express(); -app.get('/api/example', (req, res) => { - throw new Error('Boom'); +app.get("/api/example", (req, res) => { + throw new Error("Boom"); }); app.use(errorHandler.resourceNotFound); diff --git a/packages/mw-error-handler/index.js b/packages/mw-error-handler/index.js index eb024a092..0cd2bc796 100644 --- a/packages/mw-error-handler/index.js +++ b/packages/mw-error-handler/index.js @@ -1 +1 @@ -module.exports = require('./lib/mw-error-handler'); +module.exports = require("./lib/mw-error-handler"); diff --git a/packages/mw-error-handler/lib/mw-error-handler.js b/packages/mw-error-handler/lib/mw-error-handler.js index d2dafe7ec..1ff23dfaa 100644 --- a/packages/mw-error-handler/lib/mw-error-handler.js +++ b/packages/mw-error-handler/lib/mw-error-handler.js @@ -1,63 +1,66 @@ -const _ = require('lodash'); -const path = require('path'); -const semver = require('semver'); -const debug = require('@tryghost/debug')('error-handler'); -const errors = require('@tryghost/errors'); -const {prepareStackForUser} = require('@tryghost/errors').utils; -const {isReqResUserSpecific, cacheControlValues} = require('@tryghost/http-cache-utils'); -const tpl = require('@tryghost/tpl'); +const _ = require("lodash"); +const path = require("path"); +const semver = require("semver"); +const debug = require("@tryghost/debug")("error-handler"); +const errors = require("@tryghost/errors"); +const { prepareStackForUser } = require("@tryghost/errors").utils; +const { isReqResUserSpecific, cacheControlValues } = require("@tryghost/http-cache-utils"); +const tpl = require("@tryghost/tpl"); const messages = { - genericError: 'An unexpected error occurred, please try again.', - invalidValue: 'Invalid value', - pageNotFound: 'Page not found', - resourceNotFound: 'Resource not found', + genericError: "An unexpected error occurred, please try again.", + invalidValue: "Invalid value", + pageNotFound: "Page not found", + resourceNotFound: "Resource not found", methodNotAcceptableVersionAhead: { - message: 'Request could not be served, the endpoint was not found.', - context: 'Provided client accept-version {acceptVersion} is ahead of current Ghost version {ghostVersion}.', - help: 'Try upgrading your Ghost install.' + message: "Request could not be served, the endpoint was not found.", + context: + "Provided client accept-version {acceptVersion} is ahead of current Ghost version {ghostVersion}.", + help: "Try upgrading your Ghost install.", }, methodNotAcceptableVersionBehind: { - message: 'Request could not be served, the endpoint was not found.', - context: 'Provided client accept-version {acceptVersion} is behind current Ghost version {ghostVersion}.', - help: 'Try upgrading your Ghost API client.' + message: "Request could not be served, the endpoint was not found.", + context: + "Provided client accept-version {acceptVersion} is behind current Ghost version {ghostVersion}.", + help: "Try upgrading your Ghost API client.", }, - badVersion: 'Requested version is not supported.', + badVersion: "Requested version is not supported.", actions: { images: { - upload: 'upload image' - } + upload: "upload image", + }, }, userMessages: { - BookshelfRelationsError: 'Database error, cannot {action}.', - InternalServerError: 'Internal server error, cannot {action}.', - IncorrectUsageError: 'Incorrect usage error, cannot {action}.', - NotFoundError: 'Resource not found error, cannot {action}.', - BadRequestError: 'Request not understood error, cannot {action}.', - UnauthorizedError: 'Authorisation error, cannot {action}.', - NoPermissionError: 'Permission error, cannot {action}.', - ValidationError: 'Validation error, cannot {action}.', - UnsupportedMediaTypeError: 'Unsupported media error, cannot {action}.', - TooManyRequestsError: 'Too many requests error, cannot {action}.', - MaintenanceError: 'Server down for maintenance, cannot {action}.', - MethodNotAllowedError: 'Method not allowed, cannot {action}.', - RequestEntityTooLargeError: 'Request too large, cannot {action}.', - TokenRevocationError: 'Token is not available, cannot {action}.', - VersionMismatchError: 'Version mismatch error, cannot {action}.', - DataExportError: 'Error exporting content.', - DataImportError: 'Duplicated entry, cannot save {action}.', - DatabaseVersionError: 'Database version compatibility error, cannot {action}.', - EmailError: 'Error sending email!', - ThemeValidationError: 'Theme validation error, cannot {action}.', - HostLimitError: 'Host Limit error, cannot {action}.', - DisabledFeatureError: 'Theme validation error, the {{{helperName}}} helper is not available. Cannot {action}.', - UpdateCollisionError: 'Saving failed! Someone else is editing this post.' + BookshelfRelationsError: "Database error, cannot {action}.", + InternalServerError: "Internal server error, cannot {action}.", + IncorrectUsageError: "Incorrect usage error, cannot {action}.", + NotFoundError: "Resource not found error, cannot {action}.", + BadRequestError: "Request not understood error, cannot {action}.", + UnauthorizedError: "Authorisation error, cannot {action}.", + NoPermissionError: "Permission error, cannot {action}.", + ValidationError: "Validation error, cannot {action}.", + UnsupportedMediaTypeError: "Unsupported media error, cannot {action}.", + TooManyRequestsError: "Too many requests error, cannot {action}.", + MaintenanceError: "Server down for maintenance, cannot {action}.", + MethodNotAllowedError: "Method not allowed, cannot {action}.", + RequestEntityTooLargeError: "Request too large, cannot {action}.", + TokenRevocationError: "Token is not available, cannot {action}.", + VersionMismatchError: "Version mismatch error, cannot {action}.", + DataExportError: "Error exporting content.", + DataImportError: "Duplicated entry, cannot save {action}.", + DatabaseVersionError: "Database version compatibility error, cannot {action}.", + EmailError: "Error sending email!", + ThemeValidationError: "Theme validation error, cannot {action}.", + HostLimitError: "Host Limit error, cannot {action}.", + DisabledFeatureError: + "Theme validation error, the {{{helperName}}} helper is not available. Cannot {action}.", + UpdateCollisionError: "Saving failed! Someone else is editing this post.", }, - UnknownError: 'Unknown error - {name}, cannot {action}.' + UnknownError: "Unknown error - {name}, cannot {action}.", }; function isDependencyInStack(dependency, err) { - const dependencyPath = path.join('node_modules', dependency); + const dependencyPath = path.join("node_modules", dependency); return err?.stack?.match(dependencyPath); } @@ -76,46 +79,49 @@ module.exports.prepareError = function prepareError(err, req, res, next) { // For everything else, we do some custom handling here if (!errors.utils.isGhostError(err)) { // Catch bookshelf empty errors and other 404s, and turn into a Ghost 404 - if ((err.statusCode && err.statusCode === 404) || err.message === 'EmptyResponse') { + if ((err.statusCode && err.statusCode === 404) || err.message === "EmptyResponse") { err = new errors.NotFoundError({ - err: err + err: err, }); - // Catch handlebars / express-hbs errors, and render them as 400, rather than 500 errors as the server isn't broken - } else if (isDependencyInStack('handlebars', err) || isDependencyInStack('express-hbs', err)) { + // Catch handlebars / express-hbs errors, and render them as 400, rather than 500 errors as the server isn't broken + } else if ( + isDependencyInStack("handlebars", err) || + isDependencyInStack("express-hbs", err) + ) { // Temporary handling of theme errors from handlebars // @TODO remove this when #10496 is solved properly err = new errors.IncorrectUsageError({ err: err, message: err.message, - statusCode: err.statusCode + statusCode: err.statusCode, }); - // Catch database errors and turn them into 500 errors, but log some useful data to sentry - } else if (isDependencyInStack('mysql2', err)) { + // Catch database errors and turn them into 500 errors, but log some useful data to sentry + } else if (isDependencyInStack("mysql2", err)) { // we don't want to return raw database errors to our users err.sqlErrorCode = err.code; - if (err.code === 'ER_WRONG_VALUE') { + if (err.code === "ER_WRONG_VALUE") { err = new errors.ValidationError({ message: tpl(messages.invalidValue), context: err.message, - err + err, }); } else { err = new errors.InternalServerError({ err: err, message: tpl(messages.genericError), statusCode: err.statusCode, - code: 'UNEXPECTED_ERROR' + code: "UNEXPECTED_ERROR", }); } - // For everything else, create a generic 500 error, with context set to the original error message + // For everything else, create a generic 500 error, with context set to the original error message } else { err = new errors.InternalServerError({ err: err, message: tpl(messages.genericError), context: err.message, statusCode: err.statusCode, - code: 'UNEXPECTED_ERROR' + code: "UNEXPECTED_ERROR", }); } } @@ -129,7 +135,8 @@ module.exports.prepareError = function prepareError(err, req, res, next) { next(err); }; -module.exports.prepareStack = function prepareStack(err, req, res, next) { // eslint-disable-line no-unused-vars +module.exports.prepareStack = function prepareStack(err, req, res, next) { + // eslint-disable-line no-unused-vars const clonedError = prepareStackForUser(err); next(clonedError); @@ -142,21 +149,24 @@ module.exports.prepareStack = function prepareStack(err, req, res, next) { // es * @param {import('express').Response} res * @param {import('express').NextFunction} next */ -module.exports.jsonErrorRenderer = function jsonErrorRenderer(err, req, res, next) { // eslint-disable-line no-unused-vars +module.exports.jsonErrorRenderer = function jsonErrorRenderer(err, req, res, next) { + // eslint-disable-line no-unused-vars const userError = prepareUserMessage(err, req); res.json({ - errors: [{ - message: userError.message, - context: userError.context || null, - type: err.errorType || null, - details: err.errorDetails || null, - property: err.property || null, - help: err.help || null, - code: err.code || null, - id: err.id || null, - ghostErrorCode: err.ghostErrorCode || null - }] + errors: [ + { + message: userError.message, + context: userError.context || null, + type: err.errorType || null, + details: err.errorDetails || null, + property: err.property || null, + help: err.help || null, + code: err.code || null, + id: err.id || null, + ghostErrorCode: err.ghostErrorCode || null, + }, + ], }); }; @@ -164,7 +174,9 @@ module.exports.jsonErrorRenderer = function jsonErrorRenderer(err, req, res, nex * * @param {String} [cacheControlHeaderValue] cache-control header value */ -module.exports.prepareErrorCacheControl = function prepareErrorCacheControl(cacheControlHeaderValue) { +module.exports.prepareErrorCacheControl = function prepareErrorCacheControl( + cacheControlHeaderValue, +) { return function prepareErrorCacheControlInner(err, req, res, next) { let cacheControl = cacheControlHeaderValue; if (!cacheControlHeaderValue) { @@ -172,13 +184,13 @@ module.exports.prepareErrorCacheControl = function prepareErrorCacheControl(cach cacheControl = cacheControlValues.private; // Do not include 'private' cache-control directive for 404 responses - if (err.statusCode === 404 && req.method === 'GET' && !isReqResUserSpecific(req, res)) { + if (err.statusCode === 404 && req.method === "GET" && !isReqResUserSpecific(req, res)) { cacheControl = cacheControlValues.noCacheDynamic; } } res.set({ - 'Cache-Control': cacheControl + "Cache-Control": cacheControl, }); next(err); @@ -188,21 +200,21 @@ module.exports.prepareErrorCacheControl = function prepareErrorCacheControl(cach const prepareUserMessage = function prepareUserMessage(err, req) { const userError = { message: err.message, - context: err.context + context: err.context, }; - const docName = _.get(req, 'frameOptions.docName'); - const method = _.get(req, 'frameOptions.method'); + const docName = _.get(req, "frameOptions.docName"); + const method = _.get(req, "frameOptions.method"); if (docName && method) { let action; const actionMap = { - browse: 'list', - read: 'read', - add: 'save', - edit: 'edit', - destroy: 'delete' + browse: "list", + read: "read", + add: "save", + edit: "edit", + destroy: "delete", }; if (_.get(messages.actions, [docName, method])) { @@ -210,8 +222,8 @@ const prepareUserMessage = function prepareUserMessage(err, req) { } else if (Object.keys(actionMap).includes(method)) { let resource = docName; - if (method !== 'browse') { - resource = resource.replace(/s$/, ''); + if (method !== "browse") { + resource = resource.replace(/s$/, ""); } action = `${actionMap[method]} ${resource}`; @@ -225,9 +237,9 @@ const prepareUserMessage = function prepareUserMessage(err, req) { } if (_.get(messages.userMessages, err.name)) { - userError.message = tpl(messages.userMessages[err.name], {action: action}); + userError.message = tpl(messages.userMessages[err.name], { action: action }); } else { - userError.message = tpl(messages.UnknownError, {action, name: err.name}); + userError.message = tpl(messages.UnknownError, { action, name: err.name }); } } } @@ -236,45 +248,43 @@ const prepareUserMessage = function prepareUserMessage(err, req) { }; module.exports.resourceNotFound = function resourceNotFound(req, res, next) { - if (req?.headers?.['accept-version'] && res.locals?.safeVersion) { + if (req?.headers?.["accept-version"] && res.locals?.safeVersion) { // Protect against invalid `Accept-Version` headers - const acceptVersionSemver = semver.coerce(req.headers['accept-version']); + const acceptVersionSemver = semver.coerce(req.headers["accept-version"]); if (!acceptVersionSemver) { - return next(new errors.BadRequestError({ - message: tpl(messages.badVersion) - })); + return next( + new errors.BadRequestError({ + message: tpl(messages.badVersion), + }), + ); } if (semver.compare(acceptVersionSemver, semver.coerce(res.locals.safeVersion)) !== 0) { const versionComparison = semver.compare( acceptVersionSemver, - semver.coerce(res.locals.safeVersion) + semver.coerce(res.locals.safeVersion), ); let notAcceptableError; if (versionComparison === 1) { notAcceptableError = new errors.RequestNotAcceptableError({ - message: tpl( - messages.methodNotAcceptableVersionAhead.message - ), + message: tpl(messages.methodNotAcceptableVersionAhead.message), context: tpl(messages.methodNotAcceptableVersionAhead.context, { - acceptVersion: req.headers['accept-version'], - ghostVersion: `v${res.locals.safeVersion}` + acceptVersion: req.headers["accept-version"], + ghostVersion: `v${res.locals.safeVersion}`, }), help: tpl(messages.methodNotAcceptableVersionAhead.help), - code: 'UPDATE_GHOST' + code: "UPDATE_GHOST", }); } else { notAcceptableError = new errors.RequestNotAcceptableError({ - message: tpl( - messages.methodNotAcceptableVersionBehind.message - ), + message: tpl(messages.methodNotAcceptableVersionBehind.message), context: tpl(messages.methodNotAcceptableVersionBehind.context, { - acceptVersion: req.headers['accept-version'], - ghostVersion: `v${res.locals.safeVersion}` + acceptVersion: req.headers["accept-version"], + ghostVersion: `v${res.locals.safeVersion}`, }), help: tpl(messages.methodNotAcceptableVersionBehind.help), - code: 'UPDATE_CLIENT' + code: "UPDATE_CLIENT", }); } @@ -282,14 +292,14 @@ module.exports.resourceNotFound = function resourceNotFound(req, res, next) { } } - next(new errors.NotFoundError({message: tpl(messages.resourceNotFound)})); + next(new errors.NotFoundError({ message: tpl(messages.resourceNotFound) })); }; module.exports.pageNotFound = function pageNotFound(req, res, next) { - next(new errors.NotFoundError({message: tpl(messages.pageNotFound)})); + next(new errors.NotFoundError({ message: tpl(messages.pageNotFound) })); }; -module.exports.handleJSONResponse = sentry => [ +module.exports.handleJSONResponse = (sentry) => [ // Make sure the error can be served module.exports.prepareError, // Add cache-control header @@ -299,10 +309,10 @@ module.exports.handleJSONResponse = sentry => [ // Format the stack for the user module.exports.prepareStack, // Render the error using JSON format - module.exports.jsonErrorRenderer + module.exports.jsonErrorRenderer, ]; -module.exports.handleHTMLResponse = sentry => [ +module.exports.handleHTMLResponse = (sentry) => [ // Make sure the error can be served module.exports.prepareError, // Add cache-control header @@ -310,5 +320,5 @@ module.exports.handleHTMLResponse = sentry => [ // Handle the error in Sentry sentry.errorHandler, // Format the stack for the user - module.exports.prepareStack + module.exports.prepareStack, ]; diff --git a/packages/mw-error-handler/package.json b/packages/mw-error-handler/package.json index 9736827f1..8456c45b6 100644 --- a/packages/mw-error-handler/package.json +++ b/packages/mw-error-handler/package.json @@ -2,8 +2,17 @@ "name": "@tryghost/mw-error-handler", "version": "3.0.3", "description": "Express middleware utilities for normalizing and rendering Ghost API errors", - "author": "Ghost Foundation", "license": "MIT", + "author": "Ghost Foundation", + "repository": { + "type": "git", + "url": "git+https://github.com/TryGhost/framework.git", + "directory": "packages/mw-error-handler" + }, + "files": [ + "index.js", + "lib" + ], "main": "index.js", "publishConfig": { "access": "public" @@ -17,13 +26,6 @@ "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", "lint:eslint": "yarn lint:code && yarn lint:test" }, - "files": [ - "index.js", - "lib" - ], - "devDependencies": { - "sinon": "21.0.3" - }, "dependencies": { "@tryghost/debug": "^2.0.3", "@tryghost/errors": "^3.0.3", @@ -32,9 +34,7 @@ "lodash": "4.17.23", "semver": "7.7.4" }, - "repository": { - "type": "git", - "url": "git+https://github.com/TryGhost/framework.git", - "directory": "packages/mw-error-handler" + "devDependencies": { + "sinon": "21.0.3" } } diff --git a/packages/mw-error-handler/test/.eslintrc.js b/packages/mw-error-handler/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/mw-error-handler/test/.eslintrc.js +++ b/packages/mw-error-handler/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/mw-error-handler/test/mw-error-handler.test.js b/packages/mw-error-handler/test/mw-error-handler.test.js index 009ab3b50..893c6e37e 100644 --- a/packages/mw-error-handler/test/mw-error-handler.test.js +++ b/packages/mw-error-handler/test/mw-error-handler.test.js @@ -1,9 +1,9 @@ -const path = require('path'); -const assert = require('assert/strict'); -const sinon = require('sinon'); +const path = require("path"); +const assert = require("assert/strict"); +const sinon = require("sinon"); -const {InternalServerError, NotFoundError} = require('@tryghost/errors'); -const {cacheControlValues} = require('@tryghost/http-cache-utils'); +const { InternalServerError, NotFoundError } = require("@tryghost/errors"); +const { cacheControlValues } = require("@tryghost/http-cache-utils"); const { prepareError, jsonErrorRenderer, @@ -12,444 +12,537 @@ const { prepareErrorCacheControl, prepareStack, resourceNotFound, - pageNotFound -} = require('..'); + pageNotFound, +} = require(".."); -describe('Prepare Error', function () { - it('Correctly prepares a non-Ghost error', async function () { +describe("Prepare Error", function () { + it("Correctly prepares a non-Ghost error", async function () { await new Promise((resolve) => { - prepareError(new Error('test!'), {}, { - set: () => {} - }, (err) => { - assert.equal(err.statusCode, 500); - assert.equal(err.name, 'InternalServerError'); - assert.equal(err.message, 'An unexpected error occurred, please try again.'); - assert.equal(err.context, 'test!'); - assert.equal(err.code, 'UNEXPECTED_ERROR'); - assert.ok(err.stack.startsWith('Error: test!')); - resolve(); - }); + prepareError( + new Error("test!"), + {}, + { + set: () => {}, + }, + (err) => { + assert.equal(err.statusCode, 500); + assert.equal(err.name, "InternalServerError"); + assert.equal(err.message, "An unexpected error occurred, please try again."); + assert.equal(err.context, "test!"); + assert.equal(err.code, "UNEXPECTED_ERROR"); + assert.ok(err.stack.startsWith("Error: test!")); + resolve(); + }, + ); }); }); - it('Correctly prepares a Ghost error', async function () { + it("Correctly prepares a Ghost error", async function () { await new Promise((resolve) => { - prepareError(new InternalServerError({message: 'Handled Error', context: 'Details'}), {}, { - set: () => {} - }, (err) => { - assert.equal(err.statusCode, 500); - assert.equal(err.name, 'InternalServerError'); - assert.equal(err.message, 'Handled Error'); - assert.equal(err.context, 'Details'); - assert.ok(err.stack.startsWith('InternalServerError: Handled Error')); - resolve(); - }); + prepareError( + new InternalServerError({ message: "Handled Error", context: "Details" }), + {}, + { + set: () => {}, + }, + (err) => { + assert.equal(err.statusCode, 500); + assert.equal(err.name, "InternalServerError"); + assert.equal(err.message, "Handled Error"); + assert.equal(err.context, "Details"); + assert.ok(err.stack.startsWith("InternalServerError: Handled Error")); + resolve(); + }, + ); }); }); - it('Correctly prepares a 404 error', async function () { - let error = {message: 'Oh dear', statusCode: 404}; + it("Correctly prepares a 404 error", async function () { + let error = { message: "Oh dear", statusCode: 404 }; await new Promise((resolve) => { - prepareError(error, {}, { - set: () => {} - }, (err) => { - assert.equal(err.statusCode, 404); - assert.equal(err.name, 'NotFoundError'); - assert.ok(err.stack.startsWith('NotFoundError: Resource could not be found')); - assert.equal(err.hideStack, true); - resolve(); - }); + prepareError( + error, + {}, + { + set: () => {}, + }, + (err) => { + assert.equal(err.statusCode, 404); + assert.equal(err.name, "NotFoundError"); + assert.ok(err.stack.startsWith("NotFoundError: Resource could not be found")); + assert.equal(err.hideStack, true); + resolve(); + }, + ); }); }); - it('Correctly prepares an error array', async function () { + it("Correctly prepares an error array", async function () { await new Promise((resolve) => { - prepareError([new Error('test!')], {}, { - set: () => {} - }, (err) => { - assert.equal(err.statusCode, 500); - assert.equal(err.name, 'InternalServerError'); - assert.ok(err.stack.startsWith('Error: test!')); - resolve(); - }); + prepareError( + [new Error("test!")], + {}, + { + set: () => {}, + }, + (err) => { + assert.equal(err.statusCode, 500); + assert.equal(err.name, "InternalServerError"); + assert.ok(err.stack.startsWith("Error: test!")); + resolve(); + }, + ); }); }); - it('Correctly prepares a handlebars error', async function () { - let error = new Error('obscure handlebars message!'); + it("Correctly prepares a handlebars error", async function () { + let error = new Error("obscure handlebars message!"); - error.stack += '\n'; - error.stack += path.join('node_modules', 'handlebars', 'something'); + error.stack += "\n"; + error.stack += path.join("node_modules", "handlebars", "something"); await new Promise((resolve) => { - prepareError(error, {}, { - set: () => {} - }, (err) => { - assert.equal(err.statusCode, 400); - assert.equal(err.name, 'IncorrectUsageError'); - // TODO: consider if the message should be trusted here - assert.equal(err.message, 'obscure handlebars message!'); - assert.ok(err.stack.startsWith('Error: obscure handlebars message!')); - resolve(); - }); + prepareError( + error, + {}, + { + set: () => {}, + }, + (err) => { + assert.equal(err.statusCode, 400); + assert.equal(err.name, "IncorrectUsageError"); + // TODO: consider if the message should be trusted here + assert.equal(err.message, "obscure handlebars message!"); + assert.ok(err.stack.startsWith("Error: obscure handlebars message!")); + resolve(); + }, + ); }); }); - it('Correctly prepares an express-hbs error', async function () { - let error = new Error('obscure express-hbs message!'); + it("Correctly prepares an express-hbs error", async function () { + let error = new Error("obscure express-hbs message!"); - error.stack += '\n'; - error.stack += path.join('node_modules', 'express-hbs', 'lib'); + error.stack += "\n"; + error.stack += path.join("node_modules", "express-hbs", "lib"); await new Promise((resolve) => { - prepareError(error, {}, { - set: () => {} - }, (err) => { - assert.equal(err.statusCode, 400); - assert.equal(err.name, 'IncorrectUsageError'); - assert.equal(err.message, 'obscure express-hbs message!'); - assert.ok(err.stack.startsWith('Error: obscure express-hbs message!')); - resolve(); - }); + prepareError( + error, + {}, + { + set: () => {}, + }, + (err) => { + assert.equal(err.statusCode, 400); + assert.equal(err.name, "IncorrectUsageError"); + assert.equal(err.message, "obscure express-hbs message!"); + assert.ok(err.stack.startsWith("Error: obscure express-hbs message!")); + resolve(); + }, + ); }); }); - it('Correctly prepares a known ER_WRONG_VALUE mysql2 error', async function () { - let error = new Error('select anything from anywhere where something = anything;'); + it("Correctly prepares a known ER_WRONG_VALUE mysql2 error", async function () { + let error = new Error("select anything from anywhere where something = anything;"); - error.stack += '\n'; - error.stack += path.join('node_modules', 'mysql2', 'lib'); - error.code = 'ER_WRONG_VALUE'; - error.sql = 'select anything from anywhere where something = anything;'; - error.sqlMessage = 'Incorrect DATETIME value: 3234234234'; + error.stack += "\n"; + error.stack += path.join("node_modules", "mysql2", "lib"); + error.code = "ER_WRONG_VALUE"; + error.sql = "select anything from anywhere where something = anything;"; + error.sqlMessage = "Incorrect DATETIME value: 3234234234"; await new Promise((resolve) => { - prepareError(error, {}, { - set: () => {} - }, (err) => { - assert.equal(err.statusCode, 422); - assert.equal(err.name, 'ValidationError'); - assert.equal(err.message, 'Invalid value'); - assert.equal(err.code, 'ER_WRONG_VALUE'); - assert.equal(err.sqlErrorCode, 'ER_WRONG_VALUE'); - assert.equal(err.sql, 'select anything from anywhere where something = anything;'); - assert.equal(err.sqlMessage, 'Incorrect DATETIME value: 3234234234'); - resolve(); - }); + prepareError( + error, + {}, + { + set: () => {}, + }, + (err) => { + assert.equal(err.statusCode, 422); + assert.equal(err.name, "ValidationError"); + assert.equal(err.message, "Invalid value"); + assert.equal(err.code, "ER_WRONG_VALUE"); + assert.equal(err.sqlErrorCode, "ER_WRONG_VALUE"); + assert.equal( + err.sql, + "select anything from anywhere where something = anything;", + ); + assert.equal(err.sqlMessage, "Incorrect DATETIME value: 3234234234"); + resolve(); + }, + ); }); }); - it('Correctly prepares an unknown mysql2 error', async function () { - let error = new Error('select anything from anywhere where something = anything;'); + it("Correctly prepares an unknown mysql2 error", async function () { + let error = new Error("select anything from anywhere where something = anything;"); - error.stack += '\n'; - error.stack += path.join('node_modules', 'mysql2', 'lib'); - error.code = 'ER_BAD_FIELD_ERROR'; - error.sql = 'select anything from anywhere where something = anything;'; - error.sqlMessage = 'Incorrect value: erororoor'; + error.stack += "\n"; + error.stack += path.join("node_modules", "mysql2", "lib"); + error.code = "ER_BAD_FIELD_ERROR"; + error.sql = "select anything from anywhere where something = anything;"; + error.sqlMessage = "Incorrect value: erororoor"; await new Promise((resolve) => { - prepareError(error, {}, { - set: () => {} - }, (err) => { - assert.equal(err.statusCode, 500); - assert.equal(err.name, 'InternalServerError'); - assert.equal(err.message, 'An unexpected error occurred, please try again.'); - assert.equal(err.code, 'UNEXPECTED_ERROR'); - assert.equal(err.sqlErrorCode, 'ER_BAD_FIELD_ERROR'); - assert.equal(err.sql, 'select anything from anywhere where something = anything;'); - assert.equal(err.sqlMessage, 'Incorrect value: erororoor'); - resolve(); - }); + prepareError( + error, + {}, + { + set: () => {}, + }, + (err) => { + assert.equal(err.statusCode, 500); + assert.equal(err.name, "InternalServerError"); + assert.equal(err.message, "An unexpected error occurred, please try again."); + assert.equal(err.code, "UNEXPECTED_ERROR"); + assert.equal(err.sqlErrorCode, "ER_BAD_FIELD_ERROR"); + assert.equal( + err.sql, + "select anything from anywhere where something = anything;", + ); + assert.equal(err.sqlMessage, "Incorrect value: erororoor"); + resolve(); + }, + ); }); }); }); -describe('Prepare Stack', function () { - it('Correctly prepares the stack for an error', async function () { +describe("Prepare Stack", function () { + it("Correctly prepares the stack for an error", async function () { await new Promise((resolve) => { - prepareStack(new Error('test!'), {}, {}, (err) => { + prepareStack(new Error("test!"), {}, {}, (err) => { // Includes "Stack Trace" text prepending human readable trace - assert.ok(err.stack.startsWith('Error: test!\nStack Trace:')); + assert.ok(err.stack.startsWith("Error: test!\nStack Trace:")); resolve(); }); }); }); }); -describe('Prepare Error Cache Control', function () { - it('Sets private cache control by default', async function () { +describe("Prepare Error Cache Control", function () { + it("Sets private cache control by default", async function () { const res = { - set: sinon.spy() + set: sinon.spy(), }; await new Promise((resolve) => { - prepareErrorCacheControl()(new Error('generic error'), {}, res, () => { + prepareErrorCacheControl()(new Error("generic error"), {}, res, () => { assert(res.set.calledOnce); - assert(res.set.calledWith({ - 'Cache-Control': cacheControlValues.private - })); + assert( + res.set.calledWith({ + "Cache-Control": cacheControlValues.private, + }), + ); resolve(); }); }); }); - it('Sets private cache-control header for user-specific 404 responses', async function () { + it("Sets private cache-control header for user-specific 404 responses", async function () { const req = { - method: 'GET', + method: "GET", get: (header) => { - if (header === 'authorization') { - return 'Basic YWxhZGRpbjpvcGVuc2VzYW1l'; + if (header === "authorization") { + return "Basic YWxhZGRpbjpvcGVuc2VzYW1l"; } - } + }, }; const res = { - set: sinon.spy() + set: sinon.spy(), }; await new Promise((resolve) => { prepareErrorCacheControl()(new NotFoundError(), req, res, () => { assert(res.set.calledOnce); - assert(res.set.calledWith({ - 'Cache-Control': cacheControlValues.private - })); + assert( + res.set.calledWith({ + "Cache-Control": cacheControlValues.private, + }), + ); resolve(); }); }); }); - it('Sets noCache cache-control header for non-user-specific 404 responses', async function () { + it("Sets noCache cache-control header for non-user-specific 404 responses", async function () { const req = { - method: 'GET', + method: "GET", get: () => { return false; - } + }, }; const res = { set: sinon.spy(), get: () => { return false; - } + }, }; await new Promise((resolve) => { prepareErrorCacheControl()(new NotFoundError(), req, res, () => { assert(res.set.calledOnce); - assert(res.set.calledWith({ - 'Cache-Control': cacheControlValues.noCacheDynamic - })); + assert( + res.set.calledWith({ + "Cache-Control": cacheControlValues.noCacheDynamic, + }), + ); resolve(); }); }); }); }); -describe('Error renderers', function () { - it('Renders JSON', async function () { +describe("Error renderers", function () { + it("Renders JSON", async function () { await new Promise((resolve) => { - jsonErrorRenderer(new Error('test!'), {}, { - json: (data) => { - assert.equal(data.errors.length, 1); - assert.equal(data.errors[0].message, 'test!'); - resolve(); - } - }, () => {}); + jsonErrorRenderer( + new Error("test!"), + {}, + { + json: (data) => { + assert.equal(data.errors.length, 1); + assert.equal(data.errors[0].message, "test!"); + resolve(); + }, + }, + () => {}, + ); }); }); - it('Handles unknown errors when preparing user message', async function () { + it("Handles unknown errors when preparing user message", async function () { await new Promise((resolve) => { - jsonErrorRenderer(new RangeError('test!'), { - frameOptions: { - docName: 'oembed', - method: 'read' - } - }, { - json: (data) => { - assert.equal(data.errors.length, 1); - assert.equal(data.errors[0].message, 'Unknown error - RangeError, cannot read oembed.'); - assert.equal(data.errors[0].context, 'test!'); - resolve(); - } - }, () => {}); + jsonErrorRenderer( + new RangeError("test!"), + { + frameOptions: { + docName: "oembed", + method: "read", + }, + }, + { + json: (data) => { + assert.equal(data.errors.length, 1); + assert.equal( + data.errors[0].message, + "Unknown error - RangeError, cannot read oembed.", + ); + assert.equal(data.errors[0].context, "test!"); + resolve(); + }, + }, + () => {}, + ); }); }); - it('Uses templates when required', async function () { + it("Uses templates when required", async function () { await new Promise((resolve) => { - jsonErrorRenderer(new InternalServerError({ - message: 'test!' - }), { - frameOptions: { - docName: 'blog', - method: 'browse' - } - }, { - json: (data) => { - assert.equal(data.errors.length, 1); - assert.equal(data.errors[0].message, 'Internal server error, cannot list blog.'); - assert.equal(data.errors[0].context, 'test!'); - resolve(); - } - }, () => {}); + jsonErrorRenderer( + new InternalServerError({ + message: "test!", + }), + { + frameOptions: { + docName: "blog", + method: "browse", + }, + }, + { + json: (data) => { + assert.equal(data.errors.length, 1); + assert.equal( + data.errors[0].message, + "Internal server error, cannot list blog.", + ); + assert.equal(data.errors[0].context, "test!"); + resolve(); + }, + }, + () => {}, + ); }); }); - it('Uses defined message + context when available', async function () { + it("Uses defined message + context when available", async function () { await new Promise((resolve) => { - jsonErrorRenderer(new InternalServerError({ - message: 'test!', - context: 'Image was too large.' - }), { - frameOptions: { - docName: 'images', - method: 'upload' - } - }, { - json: (data) => { - assert.equal(data.errors.length, 1); - assert.equal(data.errors[0].message, 'Internal server error, cannot upload image.'); - assert.equal(data.errors[0].context, 'test! Image was too large.'); - resolve(); - } - }, () => {}); + jsonErrorRenderer( + new InternalServerError({ + message: "test!", + context: "Image was too large.", + }), + { + frameOptions: { + docName: "images", + method: "upload", + }, + }, + { + json: (data) => { + assert.equal(data.errors.length, 1); + assert.equal( + data.errors[0].message, + "Internal server error, cannot upload image.", + ); + assert.equal(data.errors[0].context, "test! Image was too large."); + resolve(); + }, + }, + () => {}, + ); }); }); - it('Exports the HTML renderer', function () { + it("Exports the HTML renderer", function () { const renderer = handleHTMLResponse({ - errorHandler: () => {} + errorHandler: () => {}, }); assert.equal(renderer.length, 4); }); - it('Exports the JSON renderer', function () { + it("Exports the JSON renderer", function () { const renderer = handleJSONResponse({ - errorHandler: () => {} + errorHandler: () => {}, }); assert.equal(renderer.length, 5); }); }); -describe('Resource Not Found', function () { - it('Returns 404 Not Found Error for a generic case', async function () { +describe("Resource Not Found", function () { + it("Returns 404 Not Found Error for a generic case", async function () { await new Promise((resolve) => { resourceNotFound({}, {}, (error) => { assert.equal(error.statusCode, 404); - assert.equal(error.message, 'Resource not found'); + assert.equal(error.message, "Resource not found"); resolve(); }); }); }); - it('Returns 406 Request Not Acceptable Error for invalid version', async function () { + it("Returns 406 Request Not Acceptable Error for invalid version", async function () { const req = { headers: { - 'accept-version': 'foo' - } + "accept-version": "foo", + }, }; const res = { locals: { - safeVersion: '4.3' - } + safeVersion: "4.3", + }, }; await new Promise((resolve) => { resourceNotFound(req, res, (error) => { assert.equal(error.statusCode, 400); - assert.equal(error.message, 'Requested version is not supported.'); + assert.equal(error.message, "Requested version is not supported."); resolve(); }); }); }); - it('Returns 406 Request Not Acceptable Error for when requested version is behind current version', async function () { + it("Returns 406 Request Not Acceptable Error for when requested version is behind current version", async function () { const req = { headers: { - 'accept-version': 'v3.9' - } + "accept-version": "v3.9", + }, }; const res = { locals: { - safeVersion: '4.3' - } + safeVersion: "4.3", + }, }; await new Promise((resolve) => { resourceNotFound(req, res, (error) => { assert.equal(error.statusCode, 406); - assert.equal(error.message, 'Request could not be served, the endpoint was not found.'); - assert.equal(error.context, 'Provided client accept-version v3.9 is behind current Ghost version v4.3.'); - assert.equal(error.help, 'Try upgrading your Ghost API client.'); + assert.equal( + error.message, + "Request could not be served, the endpoint was not found.", + ); + assert.equal( + error.context, + "Provided client accept-version v3.9 is behind current Ghost version v4.3.", + ); + assert.equal(error.help, "Try upgrading your Ghost API client."); resolve(); }); }); }); - it('Returns 406 Request Not Acceptable Error for when requested version is ahead current version', async function () { + it("Returns 406 Request Not Acceptable Error for when requested version is ahead current version", async function () { const req = { headers: { - 'accept-version': 'v4.8' - } + "accept-version": "v4.8", + }, }; const res = { locals: { - safeVersion: '4.3' - } + safeVersion: "4.3", + }, }; await new Promise((resolve) => { resourceNotFound(req, res, (error) => { assert.equal(error.statusCode, 406); - assert.equal(error.message, 'Request could not be served, the endpoint was not found.'); - assert.equal(error.context, 'Provided client accept-version v4.8 is ahead of current Ghost version v4.3.'); - assert.equal(error.help, 'Try upgrading your Ghost install.'); + assert.equal( + error.message, + "Request could not be served, the endpoint was not found.", + ); + assert.equal( + error.context, + "Provided client accept-version v4.8 is ahead of current Ghost version v4.3.", + ); + assert.equal(error.help, "Try upgrading your Ghost install."); resolve(); }); }); }); - it('Returns 404 Not Found Error for when requested version is the same as current version', async function () { + it("Returns 404 Not Found Error for when requested version is the same as current version", async function () { const req = { headers: { - 'accept-version': 'v4.3' - } + "accept-version": "v4.3", + }, }; const res = { locals: { - safeVersion: '4.3' - } + safeVersion: "4.3", + }, }; await new Promise((resolve) => { resourceNotFound(req, res, (error) => { assert.equal(error.statusCode, 404); - assert.equal(error.message, 'Resource not found'); + assert.equal(error.message, "Resource not found"); resolve(); }); }); }); - describe('pageNotFound', function () { - it('returns 404 with special message when message not set', async function () { + describe("pageNotFound", function () { + it("returns 404 with special message when message not set", async function () { await new Promise((resolve) => { pageNotFound({}, {}, (error) => { assert.equal(error.statusCode, 404); - assert.equal(error.message, 'Page not found'); + assert.equal(error.message, "Page not found"); resolve(); }); }); }); - it('returns 404 with special message even if message is set', async function () { + it("returns 404 with special message even if message is set", async function () { await new Promise((resolve) => { - pageNotFound({message: 'uh oh'}, {}, (error) => { + pageNotFound({ message: "uh oh" }, {}, (error) => { assert.equal(error.statusCode, 404); - assert.equal(error.message, 'Page not found'); + assert.equal(error.message, "Page not found"); resolve(); }); }); diff --git a/packages/mw-vhost/README.md b/packages/mw-vhost/README.md index 0fe9dbfca..3e55ec4f3 100644 --- a/packages/mw-vhost/README.md +++ b/packages/mw-vhost/README.md @@ -4,7 +4,6 @@ Virtual-host Express middleware (forked from `vhost`) with Ghost-specific trust-proxy behavior. - Forked from https://github.com/expressjs/vhost/ which appears abandoned. ## API @@ -12,7 +11,7 @@ Forked from https://github.com/expressjs/vhost/ which appears abandoned. ```js -var vhost = require('vhost') +var vhost = require("vhost"); ``` ### vhost(hostname, handle) @@ -36,18 +35,20 @@ If you're running a raw Node.js/connect server, this comes from [`req.headers.ho If you're running an express v4 server, this comes from [`req.hostname`](http://expressjs.com/en/4x/api.html#req.hostname). ```js -var connect = require('connect') -var vhost = require('vhost') -var app = connect() - -app.use(vhost('*.*.example.com', function handle (req, res, next) { - // for match of "foo.bar.example.com:8080" against "*.*.example.com": - console.dir(req.vhost.host) // => 'foo.bar.example.com:8080' - console.dir(req.vhost.hostname) // => 'foo.bar.example.com' - console.dir(req.vhost.length) // => 2 - console.dir(req.vhost[0]) // => 'foo' - console.dir(req.vhost[1]) // => 'bar' -})) +var connect = require("connect"); +var vhost = require("vhost"); +var app = connect(); + +app.use( + vhost("*.*.example.com", function handle(req, res, next) { + // for match of "foo.bar.example.com:8080" against "*.*.example.com": + console.dir(req.vhost.host); // => 'foo.bar.example.com:8080' + console.dir(req.vhost.hostname); // => 'foo.bar.example.com' + console.dir(req.vhost.length); // => 2 + console.dir(req.vhost[0]); // => 'foo' + console.dir(req.vhost[1]); // => 'bar' + }), +); ``` ## Examples @@ -55,98 +56,102 @@ app.use(vhost('*.*.example.com', function handle (req, res, next) { ### using with connect for static serving ```js -var connect = require('connect') -var serveStatic = require('serve-static') -var vhost = require('vhost') +var connect = require("connect"); +var serveStatic = require("serve-static"); +var vhost = require("vhost"); -var mailapp = connect() +var mailapp = connect(); // add middlewares to mailapp for mail.example.com // create app to serve static files on subdomain -var staticapp = connect() -staticapp.use(serveStatic('public')) +var staticapp = connect(); +staticapp.use(serveStatic("public")); // create main app -var app = connect() +var app = connect(); // add vhost routing to main app for mail -app.use(vhost('mail.example.com', mailapp)) +app.use(vhost("mail.example.com", mailapp)); // route static assets for "assets-*" subdomain to get // around max host connections limit on browsers -app.use(vhost('assets-*.example.com', staticapp)) +app.use(vhost("assets-*.example.com", staticapp)); // add middlewares and main usage to app -app.listen(3000) +app.listen(3000); ``` ### using with connect for user subdomains ```js -var connect = require('connect') -var serveStatic = require('serve-static') -var vhost = require('vhost') +var connect = require("connect"); +var serveStatic = require("serve-static"); +var vhost = require("vhost"); -var mainapp = connect() +var mainapp = connect(); // add middlewares to mainapp for the main web site // create app that will server user content from public/{username}/ -var userapp = connect() +var userapp = connect(); userapp.use(function (req, res, next) { - var username = req.vhost[0] // username is the "*" + var username = req.vhost[0]; // username is the "*" - // pretend request was for /{username}/* for file serving - req.originalUrl = req.url - req.url = '/' + username + req.url + // pretend request was for /{username}/* for file serving + req.originalUrl = req.url; + req.url = "/" + username + req.url; - next() -}) -userapp.use(serveStatic('public')) + next(); +}); +userapp.use(serveStatic("public")); // create main app -var app = connect() +var app = connect(); // add vhost routing for main app -app.use(vhost('userpages.local', mainapp)) -app.use(vhost('www.userpages.local', mainapp)) +app.use(vhost("userpages.local", mainapp)); +app.use(vhost("www.userpages.local", mainapp)); // listen on all subdomains for user pages -app.use(vhost('*.userpages.local', userapp)) +app.use(vhost("*.userpages.local", userapp)); -app.listen(3000) +app.listen(3000); ``` ### using with any generic request handler ```js -var connect = require('connect') -var http = require('http') -var vhost = require('vhost') +var connect = require("connect"); +var http = require("http"); +var vhost = require("vhost"); // create main app -var app = connect() +var app = connect(); -app.use(vhost('mail.example.com', function (req, res) { - // handle req + res belonging to mail.example.com - res.setHeader('Content-Type', 'text/plain') - res.end('hello from mail!') -})) +app.use( + vhost("mail.example.com", function (req, res) { + // handle req + res belonging to mail.example.com + res.setHeader("Content-Type", "text/plain"); + res.end("hello from mail!"); + }), +); // an external api server in any framework var httpServer = http.createServer(function (req, res) { - res.setHeader('Content-Type', 'text/plain') - res.end('hello from the api!') -}) - -app.use(vhost('api.example.com', function (req, res) { - // handle req + res belonging to api.example.com - // pass the request to a standard Node.js HTTP server - httpServer.emit('request', req, res) -})) - -app.listen(3000) + res.setHeader("Content-Type", "text/plain"); + res.end("hello from the api!"); +}); + +app.use( + vhost("api.example.com", function (req, res) { + // handle req + res belonging to api.example.com + // pass the request to a standard Node.js HTTP server + httpServer.emit("request", req, res); + }), +); + +app.listen(3000); ``` diff --git a/packages/mw-vhost/index.js b/packages/mw-vhost/index.js index d23fc9054..6ebdb4275 100644 --- a/packages/mw-vhost/index.js +++ b/packages/mw-vhost/index.js @@ -1 +1 @@ -module.exports = require('./lib/vhost'); +module.exports = require("./lib/vhost"); diff --git a/packages/mw-vhost/lib/vhost.js b/packages/mw-vhost/lib/vhost.js index b42f68f2c..6c43f4593 100644 --- a/packages/mw-vhost/lib/vhost.js +++ b/packages/mw-vhost/lib/vhost.js @@ -8,8 +8,7 @@ * MIT Licensed */ - -'use strict'; +"use strict"; /** * Module exports. @@ -24,10 +23,10 @@ module.exports = vhost; */ var ASTERISK_REGEXP = /\*/g; -var ASTERISK_REPLACE = '([^.]+)'; +var ASTERISK_REPLACE = "([^.]+)"; var END_ANCHORED_REGEXP = /(?:^|[^\\])(?:\\\\)*\$$/; var ESCAPE_REGEXP = /([.+?^=!:${}()|[\]/\\])/g; -var ESCAPE_REPLACE = '\\$1'; +var ESCAPE_REPLACE = "\\$1"; /** * Create a vhost middleware. @@ -40,15 +39,15 @@ var ESCAPE_REPLACE = '\\$1'; function vhost(hostname, handle) { if (!hostname) { - throw new TypeError('argument hostname is required'); + throw new TypeError("argument hostname is required"); } if (!handle) { - throw new TypeError('argument handle is required'); + throw new TypeError("argument handle is required"); } - if (typeof handle !== 'function') { - throw new TypeError('argument handle must be a function'); + if (typeof handle !== "function") { + throw new TypeError("argument handle must be a function"); } // create regular expression for hostname @@ -87,14 +86,10 @@ function hostnameof(req) { return; } - var offset = host[0] === '[' - ? host.indexOf(']') + 1 - : 0; - var index = host.indexOf(':', offset); + var offset = host[0] === "[" ? host.indexOf("]") + 1 : 0; + var index = host.indexOf(":", offset); - return index !== -1 - ? host.substring(0, index) - : host; + return index !== -1 ? host.substring(0, index) : host; } /** @@ -106,7 +101,7 @@ function hostnameof(req) { */ function isregexp(val) { - return Object.prototype.toString.call(val) === '[object RegExp]'; + return Object.prototype.toString.call(val) === "[object RegExp]"; } /** @@ -118,20 +113,22 @@ function isregexp(val) { function hostregexp(val) { var source = !isregexp(val) - ? String(val).replace(ESCAPE_REGEXP, ESCAPE_REPLACE).replace(ASTERISK_REGEXP, ASTERISK_REPLACE) + ? String(val) + .replace(ESCAPE_REGEXP, ESCAPE_REPLACE) + .replace(ASTERISK_REGEXP, ASTERISK_REPLACE) : val.source; // force leading anchor matching - if (source[0] !== '^') { - source = '^' + source; + if (source[0] !== "^") { + source = "^" + source; } // force trailing anchor matching if (!END_ANCHORED_REGEXP.test(source)) { - source += '$'; + source += "$"; } - return new RegExp(source, 'i'); + return new RegExp(source, "i"); } /** diff --git a/packages/mw-vhost/package.json b/packages/mw-vhost/package.json index 64dd9fbb6..a983425f4 100644 --- a/packages/mw-vhost/package.json +++ b/packages/mw-vhost/package.json @@ -1,8 +1,17 @@ { "name": "@tryghost/mw-vhost", "version": "3.0.3", - "author": "Ghost Foundation", "license": "MIT", + "author": "Ghost Foundation", + "repository": { + "type": "git", + "url": "git+https://github.com/TryGhost/framework.git", + "directory": "packages/mw-vhost" + }, + "files": [ + "index.js", + "lib" + ], "main": "index.js", "publishConfig": { "access": "public" @@ -16,16 +25,7 @@ "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", "lint:eslint": "yarn lint:code && yarn lint:test" }, - "files": [ - "index.js", - "lib" - ], "devDependencies": { "supertest": "7.2.2" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/TryGhost/framework.git", - "directory": "packages/mw-vhost" } } diff --git a/packages/mw-vhost/test/.eslintrc.js b/packages/mw-vhost/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/mw-vhost/test/.eslintrc.js +++ b/packages/mw-vhost/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/mw-vhost/test/vhost.test.js b/packages/mw-vhost/test/vhost.test.js index 091c80f56..d98f5db23 100644 --- a/packages/mw-vhost/test/vhost.test.js +++ b/packages/mw-vhost/test/vhost.test.js @@ -1,183 +1,183 @@ -const assert = require('assert/strict'); -const vhost = require('..'); +const assert = require("assert/strict"); +const vhost = require(".."); -describe('vhost(hostname, server)', function () { - it('should route by Host', async function () { +describe("vhost(hostname, server)", function () { + it("should route by Host", async function () { const vhosts = []; - vhosts.push(vhost('tobi.com', tobi)); - vhosts.push(vhost('loki.com', loki)); + vhosts.push(vhost("tobi.com", tobi)); + vhosts.push(vhost("loki.com", loki)); const app = createServer(vhosts); function tobi(req, res) { - res.end('tobi'); + res.end("tobi"); } function loki(req, res) { - res.end('loki'); + res.end("loki"); } - const response = await dispatch(app, {host: 'tobi.com'}); + const response = await dispatch(app, { host: "tobi.com" }); assert.equal(response.statusCode, 200); - assert.equal(response.body, 'tobi'); + assert.equal(response.body, "tobi"); }); - it('should route by `req.hostname` (express v4)', async function () { + it("should route by `req.hostname` (express v4)", async function () { const vhosts = []; - vhosts.push(vhost('anotherhost.com', anotherhost)); - vhosts.push(vhost('loki.com', loki)); + vhosts.push(vhost("anotherhost.com", anotherhost)); + vhosts.push(vhost("loki.com", loki)); const app = createServer(vhosts, null, function (req) { // simulate express setting req.hostname based on x-forwarded-host - req.hostname = 'anotherhost.com'; + req.hostname = "anotherhost.com"; }); function anotherhost(req, res) { - res.end('anotherhost'); + res.end("anotherhost"); } function loki(req, res) { - res.end('loki'); + res.end("loki"); } - const response = await dispatch(app, {host: 'loki.com'}); + const response = await dispatch(app, { host: "loki.com" }); assert.equal(response.statusCode, 200); - assert.equal(response.body, 'anotherhost'); + assert.equal(response.body, "anotherhost"); }); - it('should ignore port in Host', async function () { - const app = createServer('tobi.com', function (req, res) { - res.end('tobi'); + it("should ignore port in Host", async function () { + const app = createServer("tobi.com", function (req, res) { + res.end("tobi"); }); - const response = await dispatch(app, {host: 'tobi.com:8080'}); + const response = await dispatch(app, { host: "tobi.com:8080" }); assert.equal(response.statusCode, 200); - assert.equal(response.body, 'tobi'); + assert.equal(response.body, "tobi"); }); - it('should support IPv6 literal in Host', async function () { - const app = createServer('[::1]', function (req, res) { - res.end('loopback'); + it("should support IPv6 literal in Host", async function () { + const app = createServer("[::1]", function (req, res) { + res.end("loopback"); }); - const response = await dispatch(app, {host: '[::1]:8080'}); + const response = await dispatch(app, { host: "[::1]:8080" }); assert.equal(response.statusCode, 200); - assert.equal(response.body, 'loopback'); + assert.equal(response.body, "loopback"); }); - it('should 404 unless matched', async function () { + it("should 404 unless matched", async function () { const vhosts = []; - vhosts.push(vhost('tobi.com', tobi)); - vhosts.push(vhost('loki.com', loki)); + vhosts.push(vhost("tobi.com", tobi)); + vhosts.push(vhost("loki.com", loki)); const app = createServer(vhosts); function tobi(req, res) { - res.end('tobi'); + res.end("tobi"); } function loki(req, res) { - res.end('loki'); + res.end("loki"); } - const response = await dispatch(app, {host: 'ferrets.com'}); + const response = await dispatch(app, { host: "ferrets.com" }); assert.equal(response.statusCode, 404); assert.equal(response.body, 'no vhost for "ferrets.com"'); }); - it('should 404 without Host header', async function () { + it("should 404 without Host header", async function () { const vhosts = []; - vhosts.push(vhost('tobi.com', tobi)); - vhosts.push(vhost('loki.com', loki)); + vhosts.push(vhost("tobi.com", tobi)); + vhosts.push(vhost("loki.com", loki)); const app = createServer(vhosts); function tobi(req, res) { - res.end('tobi'); + res.end("tobi"); } function loki(req, res) { - res.end('loki'); + res.end("loki"); } - const response = await dispatch(app, {host: undefined}); + const response = await dispatch(app, { host: undefined }); assert.equal(response.statusCode, 404); assert.equal(response.body, 'no vhost for "undefined"'); }); - describe('arguments', function () { - describe('hostname', function () { - it('should be required', function () { + describe("arguments", function () { + describe("hostname", function () { + it("should be required", function () { assert.throws(vhost.bind(), /hostname.*required/); }); - it('should accept string', function () { - assert.doesNotThrow(vhost.bind(null, 'loki.com', function () {})); + it("should accept string", function () { + assert.doesNotThrow(vhost.bind(null, "loki.com", function () {})); }); - it('should accept RegExp', function () { + it("should accept RegExp", function () { assert.doesNotThrow(vhost.bind(null, /loki\.com/, function () {})); }); }); - describe('handle', function () { - it('should be required', function () { - assert.throws(vhost.bind(null, 'loki.com'), /handle.*required/); + describe("handle", function () { + it("should be required", function () { + assert.throws(vhost.bind(null, "loki.com"), /handle.*required/); }); - it('should accept function', function () { - assert.doesNotThrow(vhost.bind(null, 'loki.com', function () {})); + it("should accept function", function () { + assert.doesNotThrow(vhost.bind(null, "loki.com", function () {})); }); - it('should reject plain object', function () { - assert.throws(vhost.bind(null, 'loki.com', {}), /handle.*function/); + it("should reject plain object", function () { + assert.throws(vhost.bind(null, "loki.com", {}), /handle.*function/); }); }); }); - describe('with string hostname', function () { - it('should support wildcards', async function () { - const app = createServer('*.ferrets.com', function (req, res) { - res.end('wildcard!'); + describe("with string hostname", function () { + it("should support wildcards", async function () { + const app = createServer("*.ferrets.com", function (req, res) { + res.end("wildcard!"); }); - const response = await dispatch(app, {host: 'loki.ferrets.com'}); + const response = await dispatch(app, { host: "loki.ferrets.com" }); assert.equal(response.statusCode, 200); - assert.equal(response.body, 'wildcard!'); + assert.equal(response.body, "wildcard!"); }); - it('should restrict wildcards to single part', async function () { - const app = createServer('*.ferrets.com', function (req, res) { - res.end('wildcard!'); + it("should restrict wildcards to single part", async function () { + const app = createServer("*.ferrets.com", function (req, res) { + res.end("wildcard!"); }); - const response = await dispatch(app, {host: 'foo.loki.ferrets.com'}); + const response = await dispatch(app, { host: "foo.loki.ferrets.com" }); assert.equal(response.statusCode, 404); assert.equal(response.body, 'no vhost for "foo.loki.ferrets.com"'); }); - it('should treat dot as a dot', async function () { - const app = createServer('a.b.com', function (req, res) { - res.end('tobi'); + it("should treat dot as a dot", async function () { + const app = createServer("a.b.com", function (req, res) { + res.end("tobi"); }); - const response = await dispatch(app, {host: 'aXb.com'}); + const response = await dispatch(app, { host: "aXb.com" }); assert.equal(response.statusCode, 404); assert.equal(response.body, 'no vhost for "aXb.com"'); }); - it('should match entire string', async function () { - const app = createServer('.com', function (req, res) { - res.end('commercial'); + it("should match entire string", async function () { + const app = createServer(".com", function (req, res) { + res.end("commercial"); }); - const response = await dispatch(app, {host: 'foo.com'}); + const response = await dispatch(app, { host: "foo.com" }); assert.equal(response.statusCode, 404); assert.equal(response.body, 'no vhost for "foo.com"'); }); - it('should populate req.vhost', async function () { - const app = createServer('user-*.*.com', function (req, res) { + it("should populate req.vhost", async function () { + const app = createServer("user-*.*.com", function (req, res) { const keys = Object.keys(req.vhost).sort(); const arr = keys.map(function (k) { return [k, req.vhost[k]]; @@ -185,24 +185,27 @@ describe('vhost(hostname, server)', function () { res.end(JSON.stringify(arr)); }); - const response = await dispatch(app, {host: 'user-bob.foo.com:8080'}); + const response = await dispatch(app, { host: "user-bob.foo.com:8080" }); assert.equal(response.statusCode, 200); - assert.equal(response.body, '[["0","bob"],["1","foo"],["host","user-bob.foo.com:8080"],["hostname","user-bob.foo.com"],["length",2]]'); + assert.equal( + response.body, + '[["0","bob"],["1","foo"],["host","user-bob.foo.com:8080"],["hostname","user-bob.foo.com"],["length",2]]', + ); }); }); - describe('with RegExp hostname', function () { - it('should match using RegExp', async function () { + describe("with RegExp hostname", function () { + it("should match using RegExp", async function () { const app = createServer(/[tl]o[bk]i\.com/, function (req, res) { - res.end('tobi'); + res.end("tobi"); }); - const response = await dispatch(app, {host: 'toki.com'}); + const response = await dispatch(app, { host: "toki.com" }); assert.equal(response.statusCode, 200); - assert.equal(response.body, 'tobi'); + assert.equal(response.body, "tobi"); }); - it('should match entire hostname', async function () { + it("should match entire hostname", async function () { const vhosts = []; vhosts.push(vhost(/\.tobi$/, tobi)); @@ -211,18 +214,18 @@ describe('vhost(hostname, server)', function () { const app = createServer(vhosts); function tobi(req, res) { - res.end('tobi'); + res.end("tobi"); } function loki(req, res) { - res.end('loki'); + res.end("loki"); } - const response = await dispatch(app, {host: 'loki.tobi.com'}); + const response = await dispatch(app, { host: "loki.tobi.com" }); assert.equal(response.statusCode, 404); assert.equal(response.body, 'no vhost for "loki.tobi.com"'); }); - it('should populate req.vhost', async function () { + it("should populate req.vhost", async function () { const app = createServer(/user-(bob|joe)\.([^.]+)\.com/, function (req, res) { const keys = Object.keys(req.vhost).sort(); const arr = keys.map(function (k) { @@ -231,17 +234,18 @@ describe('vhost(hostname, server)', function () { res.end(JSON.stringify(arr)); }); - const response = await dispatch(app, {host: 'user-bob.foo.com:8080'}); + const response = await dispatch(app, { host: "user-bob.foo.com:8080" }); assert.equal(response.statusCode, 200); - assert.equal(response.body, '[["0","bob"],["1","foo"],["host","user-bob.foo.com:8080"],["hostname","user-bob.foo.com"],["length",2]]'); + assert.equal( + response.body, + '[["0","bob"],["1","foo"],["host","user-bob.foo.com:8080"],["hostname","user-bob.foo.com"],["length",2]]', + ); }); }); }); function createServer(hostname, server, pretest) { - const vhosts = !Array.isArray(hostname) - ? [vhost(hostname, server)] - : hostname; + const vhosts = !Array.isArray(hostname) ? [vhost(hostname, server)] : hostname; return function onRequest(req, res) { // This allows you to perform changes to the request/response @@ -256,7 +260,7 @@ function createServer(hostname, server, pretest) { index = index + 1; if (!foundVhost || err) { - res.statusCode = err ? (err.status || 500) : 404; + res.statusCode = err ? err.status || 500 : 404; res.end(err ? err.message : `no vhost for "${req.headers.host}"`); return; } @@ -268,10 +272,10 @@ function createServer(hostname, server, pretest) { }; } -function dispatch(app, {host}) { +function dispatch(app, { host }) { return new Promise((resolve, reject) => { const req = { - headers: {host} + headers: { host }, }; const res = { @@ -279,9 +283,9 @@ function dispatch(app, {host}) { end(body) { resolve({ statusCode: this.statusCode, - body: body || '' + body: body || "", }); - } + }, }; try { diff --git a/packages/nodemailer/README.md b/packages/nodemailer/README.md index 85e082122..76a0184aa 100644 --- a/packages/nodemailer/README.md +++ b/packages/nodemailer/README.md @@ -11,11 +11,13 @@ Provides pre-configured transport options for common transport services. ### Supported Transport Types 1. `smtp` - send via SMTP server. - * Detects when service: 'sendmail' is used and enables sendmail mode + +- Detects when service: 'sendmail' is used and enables sendmail mode 2. `mailgun` - Allows using Mailgun with API key instead of via SMTP. - * `auth: { api_key: 'your-mailgun-api-key' }` - * Defaults to 60-second timeout. + +- `auth: { api_key: 'your-mailgun-api-key' }` +- Defaults to 60-second timeout. 3. `sendmail` - use local sendmail binary. @@ -36,23 +38,19 @@ Factory for creating Nodemailer transports across SMTP, SES, Mailgun, Sendmail, This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests +# Copyright & License - - -# Copyright & License - -Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/nodemailer/index.js b/packages/nodemailer/index.js index d8a464b6b..b2639843d 100644 --- a/packages/nodemailer/index.js +++ b/packages/nodemailer/index.js @@ -1 +1 @@ -module.exports = require('./lib/nodemailer'); +module.exports = require("./lib/nodemailer"); diff --git a/packages/nodemailer/lib/nodemailer.js b/packages/nodemailer/lib/nodemailer.js index 5c50cd05a..445ff30ef 100644 --- a/packages/nodemailer/lib/nodemailer.js +++ b/packages/nodemailer/lib/nodemailer.js @@ -1,11 +1,11 @@ /* eslint-disable no-case-declarations */ -const errors = require('@tryghost/errors'); -const nodemailer = require('nodemailer'); -const tpl = require('@tryghost/tpl'); +const errors = require("@tryghost/errors"); +const nodemailer = require("nodemailer"); +const tpl = require("@tryghost/tpl"); const messages = { - unknownTransport: `Unknown mail transport: {transport}` + unknownTransport: `Unknown mail transport: {transport}`, }; /** @@ -19,75 +19,76 @@ module.exports = function (transport, options = {}) { transport = transport.toLowerCase(); switch (transport) { - case 'smtp': - transportOptions = options; - - /** - * @deprecated `secureConnection` was removed in Nodemailer 1.0.2 - * in favor of `secure` but Ghost has been recommending `secureConnection` - * and it's difficult to get everyone to switch at this point - * - * Therefore, we have to alias it here to keep things working - */ - if (Object.prototype.hasOwnProperty.call(options, 'secureConnection')) { - transportOptions.secure = options.secureConnection; - } - - if (options.service && options.service.toLowerCase() === 'sendmail') { + case "smtp": + transportOptions = options; + + /** + * @deprecated `secureConnection` was removed in Nodemailer 1.0.2 + * in favor of `secure` but Ghost has been recommending `secureConnection` + * and it's difficult to get everyone to switch at this point + * + * Therefore, we have to alias it here to keep things working + */ + if (Object.prototype.hasOwnProperty.call(options, "secureConnection")) { + transportOptions.secure = options.secureConnection; + } + + if (options.service && options.service.toLowerCase() === "sendmail") { + transportOptions.sendmail = true; + } + break; + case "mailgun": + const mailgunTransport = require("nodemailer-mailgun-transport"); + + // Default to 60s timeout if not set in `options` + if (!Object.prototype.hasOwnProperty.call(options, "timeout")) { + options.timeout = 60000; + } + + transportOptions = mailgunTransport(options); + break; + case "sendmail": + transportOptions = options; transportOptions.sendmail = true; - } - break; - case 'mailgun': - const mailgunTransport = require('nodemailer-mailgun-transport'); - - // Default to 60s timeout if not set in `options` - if (!Object.prototype.hasOwnProperty.call(options, 'timeout')) { - options.timeout = 60000; - } - - transportOptions = mailgunTransport(options); - break; - case 'sendmail': - transportOptions = options; - transportOptions.sendmail = true; - break; - case 'ses': - const {SESv2Client, SendRawEmailCommand} = require('@aws-sdk/client-sesv2'); - - const pattern = /(.*)email(.*)\.(.*).amazonaws.com/i; - const result = pattern.exec(options.ServiceUrl); - const region = options.region || (result && result[3]) || 'us-east-1'; - - const accessKeyId = options.accessKeyId || options.AWSAccessKeyID; - const secretAccessKey = options.secretAccessKey || options.AWSSecretKey; - const credentials = (accessKeyId && secretAccessKey) ? {accessKeyId, secretAccessKey} : undefined; - - const sesClient = new SESv2Client({ - region, - credentials - }); - - transportOptions = { - SES: {sesClient, SendRawEmailCommand} - }; - - break; - case 'direct': - transportOptions = Object.assign({direct: true}, options); - break; - case 'stub': - const stubTransport = require('nodemailer-stub-transport'); - transportOptions = stubTransport(options); - break; - default: - throw new errors.EmailError({ - message: tpl(messages.unknownTransport, {transport}) - }); + break; + case "ses": + const { SESv2Client, SendRawEmailCommand } = require("@aws-sdk/client-sesv2"); + + const pattern = /(.*)email(.*)\.(.*).amazonaws.com/i; + const result = pattern.exec(options.ServiceUrl); + const region = options.region || (result && result[3]) || "us-east-1"; + + const accessKeyId = options.accessKeyId || options.AWSAccessKeyID; + const secretAccessKey = options.secretAccessKey || options.AWSSecretKey; + const credentials = + accessKeyId && secretAccessKey ? { accessKeyId, secretAccessKey } : undefined; + + const sesClient = new SESv2Client({ + region, + credentials, + }); + + transportOptions = { + SES: { sesClient, SendRawEmailCommand }, + }; + + break; + case "direct": + transportOptions = Object.assign({ direct: true }, options); + break; + case "stub": + const stubTransport = require("nodemailer-stub-transport"); + transportOptions = stubTransport(options); + break; + default: + throw new errors.EmailError({ + message: tpl(messages.unknownTransport, { transport }), + }); } const transporter = nodemailer.createTransport(transportOptions); - if (transport === 'smtp') { + if (transport === "smtp") { Object.assign(transporter.transporter.options, options); } diff --git a/packages/nodemailer/package.json b/packages/nodemailer/package.json index 4d0607ece..3fa522e50 100644 --- a/packages/nodemailer/package.json +++ b/packages/nodemailer/package.json @@ -1,30 +1,27 @@ { "name": "@tryghost/nodemailer", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/nodemailer" }, - "author": "Ghost Foundation", - "license": "MIT", - "main": "index.js", - "scripts": { - "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" - }, "files": [ "index.js", "lib" ], + "main": "index.js", "publishConfig": { "access": "public" }, - "devDependencies": { - "sinon": "21.0.3" + "scripts": { + "dev": "echo \"Implement me!\"", + "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "dependencies": { "@aws-sdk/client-sesv2": "3.1014.0", @@ -32,5 +29,8 @@ "nodemailer": "8.0.3", "nodemailer-mailgun-transport": "2.1.5", "nodemailer-stub-transport": "1.1.0" + }, + "devDependencies": { + "sinon": "21.0.3" } } diff --git a/packages/nodemailer/test/.eslintrc.js b/packages/nodemailer/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/nodemailer/test/.eslintrc.js +++ b/packages/nodemailer/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/nodemailer/test/transporter.test.js b/packages/nodemailer/test/transporter.test.js index 6a410c776..a3ff501f1 100644 --- a/packages/nodemailer/test/transporter.test.js +++ b/packages/nodemailer/test/transporter.test.js @@ -1,115 +1,121 @@ -const assert = require('assert/strict'); -const sinon = require('sinon'); -const aws = require('@aws-sdk/client-sesv2'); -const nodemailer = require('../'); +const assert = require("assert/strict"); +const sinon = require("sinon"); +const aws = require("@aws-sdk/client-sesv2"); +const nodemailer = require("../"); const sandbox = sinon.createSandbox(); -describe('Transporter', function () { +describe("Transporter", function () { afterEach(function () { sandbox.restore(); }); - it('can create an SMTP transporter', function () { - const transporter = nodemailer('SMTP', {}); - assert.equal(transporter.transporter.name, 'SMTP'); + it("can create an SMTP transporter", function () { + const transporter = nodemailer("SMTP", {}); + assert.equal(transporter.transporter.name, "SMTP"); }); - it('can create an SMTP transporter with deprecated secureConnection (true)', function () { - const transporter = nodemailer('SMTP', {secureConnection: true}); - assert.equal(transporter.transporter.name, 'SMTP'); + it("can create an SMTP transporter with deprecated secureConnection (true)", function () { + const transporter = nodemailer("SMTP", { secureConnection: true }); + assert.equal(transporter.transporter.name, "SMTP"); assert.equal(transporter.transporter.options.secure, true); }); - it('can create an SMTP transporter with deprecated secureConnection (false)', function () { - const transporter = nodemailer('SMTP', {secureConnection: false}); - assert.equal(transporter.transporter.name, 'SMTP'); + it("can create an SMTP transporter with deprecated secureConnection (false)", function () { + const transporter = nodemailer("SMTP", { secureConnection: false }); + assert.equal(transporter.transporter.name, "SMTP"); assert.equal(transporter.transporter.options.secure, false); }); - it('can create an SMTP transporter with Sendmail service', function () { - const transporter = nodemailer('SMTP', {service: 'Sendmail'}); - assert.equal(transporter.transporter.name, 'Sendmail'); + it("can create an SMTP transporter with Sendmail service", function () { + const transporter = nodemailer("SMTP", { service: "Sendmail" }); + assert.equal(transporter.transporter.name, "Sendmail"); }); - it('can create an Sendmail transporter', function () { - const transporter = nodemailer('Sendmail', {}); - assert.equal(transporter.transporter.name, 'Sendmail'); + it("can create an Sendmail transporter", function () { + const transporter = nodemailer("Sendmail", {}); + assert.equal(transporter.transporter.name, "Sendmail"); }); - it('can create an SES transporter', function () { - const transporter = nodemailer('SES', {}); - assert.equal(transporter.transporter.name, 'SESTransport'); + it("can create an SES transporter", function () { + const transporter = nodemailer("SES", {}); + assert.equal(transporter.transporter.name, "SESTransport"); }); - it('can create an SES transporter with region parsed from ServiceUrl and aws-style credentials', function () { - const sesStub = sandbox.stub(aws, 'SESv2Client').callsFake(function SESv2Client(options) { + it("can create an SES transporter with region parsed from ServiceUrl and aws-style credentials", function () { + const sesStub = sandbox.stub(aws, "SESv2Client").callsFake(function SESv2Client(options) { this.options = options; }); - nodemailer('SES', { - ServiceUrl: 'email-smtp.eu-west-2.amazonaws.com', - AWSAccessKeyID: 'key', - AWSSecretKey: 'secret' + nodemailer("SES", { + ServiceUrl: "email-smtp.eu-west-2.amazonaws.com", + AWSAccessKeyID: "key", + AWSSecretKey: "secret", }); assert.equal(sesStub.calledOnce, true); - assert.equal(sesStub.args[0][0].region, 'eu-west-2'); - assert.deepEqual(sesStub.args[0][0].credentials, {accessKeyId: 'key', secretAccessKey: 'secret'}); + assert.equal(sesStub.args[0][0].region, "eu-west-2"); + assert.deepEqual(sesStub.args[0][0].credentials, { + accessKeyId: "key", + secretAccessKey: "secret", + }); }); - it('can create an SES transporter with explicit region and modern credentials', function () { - const sesStub = sandbox.stub(aws, 'SESv2Client').callsFake(function SESv2Client(options) { + it("can create an SES transporter with explicit region and modern credentials", function () { + const sesStub = sandbox.stub(aws, "SESv2Client").callsFake(function SESv2Client(options) { this.options = options; }); - nodemailer('SES', { - ServiceUrl: 'email-smtp.us-east-1.amazonaws.com', - region: 'eu-central-1', - accessKeyId: 'new-key', - secretAccessKey: 'new-secret' + nodemailer("SES", { + ServiceUrl: "email-smtp.us-east-1.amazonaws.com", + region: "eu-central-1", + accessKeyId: "new-key", + secretAccessKey: "new-secret", }); assert.equal(sesStub.calledOnce, true); - assert.equal(sesStub.args[0][0].region, 'eu-central-1'); - assert.deepEqual(sesStub.args[0][0].credentials, {accessKeyId: 'new-key', secretAccessKey: 'new-secret'}); + assert.equal(sesStub.args[0][0].region, "eu-central-1"); + assert.deepEqual(sesStub.args[0][0].credentials, { + accessKeyId: "new-key", + secretAccessKey: "new-secret", + }); }); - it('can create a Direct transporter', function () { - const transporter = nodemailer('direct', {}); - assert.equal(transporter.transporter.name, 'SMTP'); + it("can create a Direct transporter", function () { + const transporter = nodemailer("direct", {}); + assert.equal(transporter.transporter.name, "SMTP"); }); - it('can create a Stub transporter', function () { - const transporter = nodemailer('stub', {}); - assert.equal(transporter.transporter.name, 'Stub'); + it("can create a Stub transporter", function () { + const transporter = nodemailer("stub", {}); + assert.equal(transporter.transporter.name, "Stub"); }); - it('can create a Mailgun transporter', function () { - const transporter = nodemailer('mailgun', { + it("can create a Mailgun transporter", function () { + const transporter = nodemailer("mailgun", { auth: { - api_key: 'hello', - domain: 'example.com' - } + api_key: "hello", + domain: "example.com", + }, }); - assert.equal(transporter.transporter.name, 'Mailgun'); + assert.equal(transporter.transporter.name, "Mailgun"); // Ensure the default timeout is set assert.equal(transporter.transporter.options.timeout, 60000); }); - it('can create a Mailgun transporter with custom timeout', function () { - const transporter = nodemailer('mailgun', { + it("can create a Mailgun transporter with custom timeout", function () { + const transporter = nodemailer("mailgun", { auth: { - api_key: 'hello', - domain: 'example.com' + api_key: "hello", + domain: "example.com", }, - timeout: 10000 + timeout: 10000, }); - assert.equal(transporter.transporter.name, 'Mailgun'); + assert.equal(transporter.transporter.name, "Mailgun"); assert.equal(transporter.transporter.options.timeout, 10000); }); - it('should throw an error when creating an unknown transporter', function () { - assert.throws(() => nodemailer('unknown', {})); + it("should throw an error when creating an unknown transporter", function () { + assert.throws(() => nodemailer("unknown", {})); }); }); diff --git a/packages/pretty-cli/index.js b/packages/pretty-cli/index.js index e9fd56a9f..8f7f9a90b 100644 --- a/packages/pretty-cli/index.js +++ b/packages/pretty-cli/index.js @@ -1 +1 @@ -module.exports = require('./lib/pretty-cli'); +module.exports = require("./lib/pretty-cli"); diff --git a/packages/pretty-cli/lib/pretty-cli.js b/packages/pretty-cli/lib/pretty-cli.js index 781194327..20b6b4c3e 100644 --- a/packages/pretty-cli/lib/pretty-cli.js +++ b/packages/pretty-cli/lib/pretty-cli.js @@ -1,6 +1,6 @@ -const Api = require('sywac/api'); -const styles = require('./styles'); -const ui = require('./ui'); +const Api = require("sywac/api"); +const styles = require("./styles"); +const ui = require("./ui"); /** * Pretty CLI * @@ -9,14 +9,14 @@ const ui = require('./ui'); // Exports a pre-configured version of sywac module.exports = Api.get() -// Use help & version with short forms AND -// group them into a Global Options group to keep them separate from per-command options - .help('-h, --help', {group: 'Global Options:'}) - .version('-v, --version', {group: 'Global Options:'}) + // Use help & version with short forms AND + // group them into a Global Options group to keep them separate from per-command options + .help("-h, --help", { group: "Global Options:" }) + .version("-v, --version", { group: "Global Options:" }) // Load our style rules .style(styles) // Add some padding at the end - .epilogue(' ') + .epilogue(" ") // If no command is passed, output the help menu .showHelpByDefault(); diff --git a/packages/pretty-cli/lib/styles.js b/packages/pretty-cli/lib/styles.js index 9857c0d08..064088f86 100644 --- a/packages/pretty-cli/lib/styles.js +++ b/packages/pretty-cli/lib/styles.js @@ -1,21 +1,21 @@ -const {default: chalk} = require('chalk'); +const { default: chalk } = require("chalk"); module.exports = { // Usage: script [options] etc usagePrefix: (str) => { - return chalk.yellow(str.slice(0, 6)) + '\n ' + str.slice(7); + return chalk.yellow(str.slice(0, 6)) + "\n " + str.slice(7); }, // Options: Arguments: etc - group: str => chalk.yellow(str), + group: (str) => chalk.yellow(str), // --help etc - flags: str => chalk.green(str), + flags: (str) => chalk.green(str), // [required] [boolean] etc - hints: str => chalk.dim(str), + hints: (str) => chalk.dim(str), // Use different style when a type is invalid - groupError: str => chalk.red(str), - flagsError: str => chalk.red(str), - descError: str => chalk.yellow(str), - hintsError: str => chalk.red(str), + groupError: (str) => chalk.red(str), + flagsError: (str) => chalk.red(str), + descError: (str) => chalk.yellow(str), + hintsError: (str) => chalk.red(str), // style error messages - messages: str => chalk.red(str) + messages: (str) => chalk.red(str), }; diff --git a/packages/pretty-cli/lib/ui.js b/packages/pretty-cli/lib/ui.js index 195726076..bdf16c48d 100644 --- a/packages/pretty-cli/lib/ui.js +++ b/packages/pretty-cli/lib/ui.js @@ -1,32 +1,32 @@ -const {default: chalk} = require('chalk'); +const { default: chalk } = require("chalk"); const log = (...args) => console.log(...args); // eslint-disable-line no-console module.exports.log = log; module.exports.log.ok = (...args) => { - log(chalk.green('ok'), ...args); + log(chalk.green("ok"), ...args); }; module.exports.log.trace = (...args) => { - log(chalk.gray('trace'), ...args); + log(chalk.gray("trace"), ...args); }; module.exports.log.debug = (...args) => { - log(chalk.gray('debug'), ...args); + log(chalk.gray("debug"), ...args); }; module.exports.log.info = (...args) => { - log(chalk.cyan('info'), ...args); + log(chalk.cyan("info"), ...args); }; module.exports.log.warn = (...args) => { - log(chalk.magenta('warn'), ...args); + log(chalk.magenta("warn"), ...args); }; module.exports.log.error = (...args) => { - log(chalk.red('error'), ...args); + log(chalk.red("error"), ...args); }; module.exports.log.fatal = (...args) => { - log(chalk.inverse('fatal'), ...args); + log(chalk.inverse("fatal"), ...args); }; diff --git a/packages/pretty-cli/package.json b/packages/pretty-cli/package.json index 72c0adc7b..63a9683f9 100644 --- a/packages/pretty-cli/package.json +++ b/packages/pretty-cli/package.json @@ -2,33 +2,33 @@ "name": "@tryghost/pretty-cli", "version": "3.0.3", "description": "A mini-module to style a sywac instance in a standard way", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/pretty-cli" }, - "author": "Ghost Foundation", - "license": "MIT", - "main": "index.js", - "scripts": { - "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" - }, "files": [ "index.js", "lib" ], + "main": "index.js", "publishConfig": { "access": "public" }, - "devDependencies": { - "sinon": "21.0.3" + "scripts": { + "dev": "echo \"Implement me!\"", + "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "dependencies": { "chalk": "5.6.2", "sywac": "1.3.0" + }, + "devDependencies": { + "sinon": "21.0.3" } } diff --git a/packages/pretty-cli/test/.eslintrc.js b/packages/pretty-cli/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/pretty-cli/test/.eslintrc.js +++ b/packages/pretty-cli/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/pretty-cli/test/pretty-cli.test.js b/packages/pretty-cli/test/pretty-cli.test.js index b4ec866ee..337ad243b 100644 --- a/packages/pretty-cli/test/pretty-cli.test.js +++ b/packages/pretty-cli/test/pretty-cli.test.js @@ -1,96 +1,99 @@ -const assert = require('assert/strict'); -const sinon = require('sinon'); -const prettyCLI = require('../'); +const assert = require("assert/strict"); +const sinon = require("sinon"); +const prettyCLI = require("../"); -describe('API', function () { - it('Exposes styled-sywac, styles & the sywac API', function () { - assert.equal(typeof prettyCLI, 'object'); - assert.equal(typeof prettyCLI.parseAndExit, 'function'); +describe("API", function () { + it("Exposes styled-sywac, styles & the sywac API", function () { + assert.equal(typeof prettyCLI, "object"); + assert.equal(typeof prettyCLI.parseAndExit, "function"); assert.notEqual(prettyCLI.types, undefined); - assert.equal(typeof prettyCLI.Api, 'function'); - assert.equal(typeof prettyCLI.Api.get, 'function'); + assert.equal(typeof prettyCLI.Api, "function"); + assert.equal(typeof prettyCLI.Api.get, "function"); - assert.equal(typeof prettyCLI.styles, 'object'); + assert.equal(typeof prettyCLI.styles, "object"); assert.deepEqual(Object.keys(prettyCLI.styles).sort(), [ - 'descError', - 'flags', - 'flagsError', - 'group', - 'groupError', - 'hints', - 'hintsError', - 'messages', - 'usagePrefix' + "descError", + "flags", + "flagsError", + "group", + "groupError", + "hints", + "hintsError", + "messages", + "usagePrefix", ]); - assert.equal(typeof prettyCLI.ui, 'object'); - assert.equal(typeof prettyCLI.ui.log, 'function'); + assert.equal(typeof prettyCLI.ui, "object"); + assert.equal(typeof prettyCLI.ui.log, "function"); assert.deepEqual(Object.keys(prettyCLI.ui.log).sort(), [ - 'debug', - 'error', - 'fatal', - 'info', - 'ok', - 'trace', - 'warn' + "debug", + "error", + "fatal", + "info", + "ok", + "trace", + "warn", ]); }); }); -describe('styles', function () { - it('usagePrefix styles first word and indents the remainder', function () { - const out = prettyCLI.styles.usagePrefix('Usage: app --help'); - assert.equal(String(out).includes('Usage:'), true); - assert.equal(String(out).includes('\n app --help'), true); +describe("styles", function () { + it("usagePrefix styles first word and indents the remainder", function () { + const out = prettyCLI.styles.usagePrefix("Usage: app --help"); + assert.equal(String(out).includes("Usage:"), true); + assert.equal(String(out).includes("\n app --help"), true); }); - it('applies style functions for standard and error states', function () { - assert.equal(String(prettyCLI.styles.group('Options:')).includes('Options:'), true); - assert.equal(String(prettyCLI.styles.flags('--help')).includes('--help'), true); - assert.equal(String(prettyCLI.styles.hints('[required]')).includes('[required]'), true); + it("applies style functions for standard and error states", function () { + assert.equal(String(prettyCLI.styles.group("Options:")).includes("Options:"), true); + assert.equal(String(prettyCLI.styles.flags("--help")).includes("--help"), true); + assert.equal(String(prettyCLI.styles.hints("[required]")).includes("[required]"), true); - assert.equal(String(prettyCLI.styles.groupError('Options:')).includes('Options:'), true); - assert.equal(String(prettyCLI.styles.flagsError('--help')).includes('--help'), true); - assert.equal(String(prettyCLI.styles.descError('bad arg')).includes('bad arg'), true); - assert.equal(String(prettyCLI.styles.hintsError('[required]')).includes('[required]'), true); - assert.equal(String(prettyCLI.styles.messages('boom')).includes('boom'), true); + assert.equal(String(prettyCLI.styles.groupError("Options:")).includes("Options:"), true); + assert.equal(String(prettyCLI.styles.flagsError("--help")).includes("--help"), true); + assert.equal(String(prettyCLI.styles.descError("bad arg")).includes("bad arg"), true); + assert.equal( + String(prettyCLI.styles.hintsError("[required]")).includes("[required]"), + true, + ); + assert.equal(String(prettyCLI.styles.messages("boom")).includes("boom"), true); }); }); -describe('ui', function () { +describe("ui", function () { let consoleLog; beforeEach(function () { - consoleLog = sinon.stub(console, 'log'); + consoleLog = sinon.stub(console, "log"); }); afterEach(function () { consoleLog.restore(); }); - it('log writes through directly', function () { - prettyCLI.ui.log('hello', 1); + it("log writes through directly", function () { + prettyCLI.ui.log("hello", 1); assert.equal(consoleLog.calledOnce, true); - assert.deepEqual(consoleLog.args[0], ['hello', 1]); + assert.deepEqual(consoleLog.args[0], ["hello", 1]); }); - it('severity helpers prefix with expected labels', function () { - prettyCLI.ui.log.ok('a'); - prettyCLI.ui.log.trace('b'); - prettyCLI.ui.log.debug('c'); - prettyCLI.ui.log.info('d'); - prettyCLI.ui.log.warn('e'); - prettyCLI.ui.log.error('f'); - prettyCLI.ui.log.fatal('g'); + it("severity helpers prefix with expected labels", function () { + prettyCLI.ui.log.ok("a"); + prettyCLI.ui.log.trace("b"); + prettyCLI.ui.log.debug("c"); + prettyCLI.ui.log.info("d"); + prettyCLI.ui.log.warn("e"); + prettyCLI.ui.log.error("f"); + prettyCLI.ui.log.fatal("g"); assert.equal(consoleLog.callCount, 7); - assert.equal(String(consoleLog.args[0][0]).includes('ok'), true); - assert.equal(String(consoleLog.args[1][0]).includes('trace'), true); - assert.equal(String(consoleLog.args[2][0]).includes('debug'), true); - assert.equal(String(consoleLog.args[3][0]).includes('info'), true); - assert.equal(String(consoleLog.args[4][0]).includes('warn'), true); - assert.equal(String(consoleLog.args[5][0]).includes('error'), true); - assert.equal(String(consoleLog.args[6][0]).includes('fatal'), true); + assert.equal(String(consoleLog.args[0][0]).includes("ok"), true); + assert.equal(String(consoleLog.args[1][0]).includes("trace"), true); + assert.equal(String(consoleLog.args[2][0]).includes("debug"), true); + assert.equal(String(consoleLog.args[3][0]).includes("info"), true); + assert.equal(String(consoleLog.args[4][0]).includes("warn"), true); + assert.equal(String(consoleLog.args[5][0]).includes("error"), true); + assert.equal(String(consoleLog.args[6][0]).includes("fatal"), true); }); }); diff --git a/packages/pretty-stream/README.md b/packages/pretty-stream/README.md index b94b38ead..016f60a2d 100644 --- a/packages/pretty-stream/README.md +++ b/packages/pretty-stream/README.md @@ -8,7 +8,6 @@ or `yarn add @tryghost/pretty-stream` - ## Purpose Pretty-print stream formatter used to render structured logs in human-readable form. @@ -22,23 +21,19 @@ Used by `@tryghost/logging` and `@tryghost/metrics`. This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests +# Copyright & License - - -# Copyright & License - -Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/pretty-stream/index.js b/packages/pretty-stream/index.js index 47d99cd83..0d7811d3f 100644 --- a/packages/pretty-stream/index.js +++ b/packages/pretty-stream/index.js @@ -1 +1 @@ -module.exports = require('./lib/PrettyStream'); +module.exports = require("./lib/PrettyStream"); diff --git a/packages/pretty-stream/lib/PrettyStream.js b/packages/pretty-stream/lib/PrettyStream.js index 1d016e0e6..5f897a358 100644 --- a/packages/pretty-stream/lib/PrettyStream.js +++ b/packages/pretty-stream/lib/PrettyStream.js @@ -1,31 +1,31 @@ -const dateFormat = require('date-format'); -const Transform = require('stream').Transform; -const format = require('util').format; -const prettyjson = require('prettyjson'); -const each = require('lodash/each'); -const get = require('lodash/get'); -const isArray = require('lodash/isArray'); -const isEmpty = require('lodash/isEmpty'); -const isObject = require('lodash/isObject'); - -const OMITTED_KEYS = ['time', 'level', 'name', 'hostname', 'pid', 'v', 'msg']; +const dateFormat = require("date-format"); +const Transform = require("stream").Transform; +const format = require("util").format; +const prettyjson = require("prettyjson"); +const each = require("lodash/each"); +const get = require("lodash/get"); +const isArray = require("lodash/isArray"); +const isEmpty = require("lodash/isEmpty"); +const isObject = require("lodash/isObject"); + +const OMITTED_KEYS = ["time", "level", "name", "hostname", "pid", "v", "msg"]; const _private = { levelFromName: { - 10: 'trace', - 20: 'debug', - 30: 'info', - 40: 'warn', - 50: 'error', - 60: 'fatal' + 10: "trace", + 20: "debug", + 30: "info", + 40: "warn", + 50: "error", + 60: "fatal", }, colorForLevel: { - 10: 'grey', - 20: 'grey', - 30: 'cyan', - 40: 'magenta', - 50: 'red', - 60: 'inverse' + 10: "grey", + 20: "grey", + 30: "cyan", + 40: "magenta", + 50: "red", + 60: "inverse", }, colors: { default: [39, 39], @@ -41,25 +41,38 @@ const _private = { green: [32, 39], magenta: [35, 39], red: [31, 39], - yellow: [33, 39] - } + yellow: [33, 39], + }, }; function colorize(colors, value) { if (isArray(colors)) { return colors.reduce((acc, color) => colorize(color, acc), value); } else { - return '\x1B[' + _private.colors[colors][0] + 'm' + value + '\x1B[' + _private.colors[colors][1] + 'm'; + return ( + "\x1B[" + + _private.colors[colors][0] + + "m" + + value + + "\x1B[" + + _private.colors[colors][1] + + "m" + ); } } function statusCode(status) { /* eslint-disable indent */ - const color = status >= 500 ? 'red' - : status >= 400 ? 'yellow' - : status >= 300 ? 'cyan' - : status >= 200 ? 'green' - : 'default'; // no color + const color = + status >= 500 + ? "red" + : status >= 400 + ? "yellow" + : status >= 300 + ? "cyan" + : status >= 200 + ? "green" + : "default"; // no color /* eslint-enable indent */ return colorize(color, status); @@ -70,7 +83,7 @@ class PrettyStream extends Transform { options = options || {}; super(options); - this.mode = options.mode || 'short'; + this.mode = options.mode || "short"; } write(data, enc, cb) { @@ -84,12 +97,12 @@ class PrettyStream extends Transform { } _transform(data, enc, cb) { - if (typeof data !== 'string') { + if (typeof data !== "string") { data = data.toString(); } // Remove trailing newline if any - data = data.replace(/\\n$/, ''); + data = data.replace(/\\n$/, ""); try { data = JSON.parse(data); @@ -99,127 +112,140 @@ class PrettyStream extends Transform { return; } - let output = ''; + let output = ""; // Handle time formatting let time; if (data.time) { // If time is provided as a string in the expected format, use it directly - if (typeof data.time === 'string' && /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(data.time)) { + if ( + typeof data.time === "string" && + /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(data.time) + ) { time = data.time; } else { // Otherwise, parse and format it const dataTime = new Date(data.time); - time = dateFormat.asString('yyyy-MM-dd hh:mm:ss', dataTime); + time = dateFormat.asString("yyyy-MM-dd hh:mm:ss", dataTime); } } else { // No time provided, use current time const now = new Date(); - time = dateFormat.asString('yyyy-MM-dd hh:mm:ss', now); + time = dateFormat.asString("yyyy-MM-dd hh:mm:ss", now); } let logLevel = _private.levelFromName[data.level].toUpperCase(); const codes = _private.colors[_private.colorForLevel[data.level]]; - let bodyPretty = ''; + let bodyPretty = ""; - logLevel = '\x1B[' + codes[0] + 'm' + logLevel + '\x1B[' + codes[1] + 'm'; + logLevel = "\x1B[" + codes[0] + "m" + logLevel + "\x1B[" + codes[1] + "m"; if (data.req) { - output += format('[%s] %s "%s %s" %s %s\n', + output += format( + '[%s] %s "%s %s" %s %s\n', time, logLevel, data.req.method.toUpperCase(), - get(data, 'req.originalUrl'), - statusCode(get(data, 'res.statusCode')), - get(data, 'res.responseTime') + get(data, "req.originalUrl"), + statusCode(get(data, "res.statusCode")), + get(data, "res.responseTime"), ); } else if (data.msg === undefined) { - output += format('[%s] %s\n', - time, - logLevel - ); + output += format("[%s] %s\n", time, logLevel); } else { bodyPretty += data.msg; - output += format('[%s] %s %s\n', time, logLevel, bodyPretty); + output += format("[%s] %s %s\n", time, logLevel, bodyPretty); } - each(Object.fromEntries(Object.entries(data).filter(([key]) => !OMITTED_KEYS.includes(key))), function (value, key) { - // we always output errors for now - if (isObject(value) && value.message && value.stack) { - let error = '\n'; - - if (value.errorType) { - error += colorize(_private.colorForLevel[data.level], 'Type: ' + value.errorType) + '\n'; - } - - error += colorize(_private.colorForLevel[data.level], value.message) + '\n\n'; - - if (value.context) { - error += colorize('white', value.context) + '\n'; - } - - if (value.help) { - error += colorize('yellow', value.help) + '\n'; - } + each( + Object.fromEntries(Object.entries(data).filter(([key]) => !OMITTED_KEYS.includes(key))), + function (value, key) { + // we always output errors for now + if (isObject(value) && value.message && value.stack) { + let error = "\n"; + + if (value.errorType) { + error += + colorize( + _private.colorForLevel[data.level], + "Type: " + value.errorType, + ) + "\n"; + } - if (value.context || value.help) { - error += '\n'; - } + error += colorize(_private.colorForLevel[data.level], value.message) + "\n\n"; - if (value.id) { - error += colorize(['white', 'bold'], 'Error ID:') + '\n'; - error += ' ' + colorize('grey', value.id) + '\n\n'; - } + if (value.context) { + error += colorize("white", value.context) + "\n"; + } - if (value.code) { - error += colorize(['white', 'bold'], 'Error Code: ') + '\n'; - error += ' ' + colorize('grey', value.code) + '\n\n'; - } + if (value.help) { + error += colorize("yellow", value.help) + "\n"; + } - if (value.errorDetails) { - let details = value.errorDetails; + if (value.context || value.help) { + error += "\n"; + } - try { - const jsonDetails = JSON.parse(value.errorDetails); - details = isArray(jsonDetails) ? jsonDetails[0] : jsonDetails; - } catch (err) { - // no need for special handling as we default to unparsed 'errorDetails' + if (value.id) { + error += colorize(["white", "bold"], "Error ID:") + "\n"; + error += " " + colorize("grey", value.id) + "\n\n"; } - const pretty = prettyjson.render(details, { - noColor: true - }, 4); + if (value.code) { + error += colorize(["white", "bold"], "Error Code: ") + "\n"; + error += " " + colorize("grey", value.code) + "\n\n"; + } - error += colorize(['white', 'bold'], 'Details:') + '\n'; - error += colorize('grey', pretty) + '\n\n'; - } + if (value.errorDetails) { + let details = value.errorDetails; + + try { + const jsonDetails = JSON.parse(value.errorDetails); + details = isArray(jsonDetails) ? jsonDetails[0] : jsonDetails; + } catch (err) { + // no need for special handling as we default to unparsed 'errorDetails' + } + + const pretty = prettyjson.render( + details, + { + noColor: true, + }, + 4, + ); + + error += colorize(["white", "bold"], "Details:") + "\n"; + error += colorize("grey", pretty) + "\n\n"; + } - if (value.stack && !value.hideStack) { - error += colorize('grey', '----------------------------------------') + '\n\n'; - error += colorize('grey', value.stack) + '\n'; - } + if (value.stack && !value.hideStack) { + error += + colorize("grey", "----------------------------------------") + "\n\n"; + error += colorize("grey", value.stack) + "\n"; + } - output += format('%s\n', colorize(_private.colorForLevel[data.level], error)); - } else if (isObject(value)) { - bodyPretty += '\n' + colorize('yellow', key.toUpperCase()) + '\n'; + output += format("%s\n", colorize(_private.colorForLevel[data.level], error)); + } else if (isObject(value)) { + bodyPretty += "\n" + colorize("yellow", key.toUpperCase()) + "\n"; - let sanitized = {}; + let sanitized = {}; - each(value, function (innerValue, innerKey) { - if (!isEmpty(innerValue)) { - sanitized[innerKey] = innerValue; - } - }); + each(value, function (innerValue, innerKey) { + if (!isEmpty(innerValue)) { + sanitized[innerKey] = innerValue; + } + }); - bodyPretty += prettyjson.render(sanitized, {}) + '\n'; - } else { - bodyPretty += prettyjson.render(value, {}) + '\n'; - } - }); + bodyPretty += prettyjson.render(sanitized, {}) + "\n"; + } else { + bodyPretty += prettyjson.render(value, {}) + "\n"; + } + }, + ); - if (this.mode !== 'short' && (bodyPretty !== data.msg)) { - output += format('%s\n', colorize('grey', bodyPretty)); + if (this.mode !== "short" && bodyPretty !== data.msg) { + output += format("%s\n", colorize("grey", bodyPretty)); } cb(null, output); diff --git a/packages/pretty-stream/package.json b/packages/pretty-stream/package.json index d7864b026..663935926 100644 --- a/packages/pretty-stream/package.json +++ b/packages/pretty-stream/package.json @@ -1,34 +1,34 @@ { "name": "@tryghost/pretty-stream", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/pretty-stream" }, - "author": "Ghost Foundation", - "license": "MIT", - "main": "index.js", - "scripts": { - "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" - }, "files": [ "index.js", "lib" ], + "main": "index.js", "publishConfig": { "access": "public" }, - "devDependencies": { - "sinon": "21.0.3" + "scripts": { + "dev": "echo \"Implement me!\"", + "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "dependencies": { "date-format": "4.0.14", "lodash": "4.17.23", "prettyjson": "1.2.5" + }, + "devDependencies": { + "sinon": "21.0.3" } } diff --git a/packages/pretty-stream/test/.eslintrc.js b/packages/pretty-stream/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/pretty-stream/test/.eslintrc.js +++ b/packages/pretty-stream/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/pretty-stream/test/PrettyStream.test.js b/packages/pretty-stream/test/PrettyStream.test.js index 113ea3177..4f07feae8 100644 --- a/packages/pretty-stream/test/PrettyStream.test.js +++ b/packages/pretty-stream/test/PrettyStream.test.js @@ -1,469 +1,535 @@ -const assert = require('assert/strict'); -const PrettyStream = require('../index'); -const Writable = require('stream').Writable; -const sinon = require('sinon'); +const assert = require("assert/strict"); +const PrettyStream = require("../index"); +const Writable = require("stream").Writable; +const sinon = require("sinon"); -describe('PrettyStream', function () { +describe("PrettyStream", function () { afterEach(function () { sinon.restore(); }); - describe('short mode', function () { - it('data.msg', async function () { + describe("short mode", function () { + it("data.msg", async function () { await new Promise((resolve) => { - var ghostPrettyStream = new PrettyStream({mode: 'short'}); + var ghostPrettyStream = new PrettyStream({ mode: "short" }); var writeStream = new Writable(); writeStream._write = function (data) { data = data.toString(); - assert.equal(data, '[2016-07-01 00:00:00] \u001b[36mINFO\u001b[39m Ghost starts now.\n'); + assert.equal( + data, + "[2016-07-01 00:00:00] \u001b[36mINFO\u001b[39m Ghost starts now.\n", + ); resolve(); }; ghostPrettyStream.pipe(writeStream); - ghostPrettyStream.write(JSON.stringify({ - time: '2016-07-01 00:00:00', - level: 30, - msg: 'Ghost starts now.' - })); + ghostPrettyStream.write( + JSON.stringify({ + time: "2016-07-01 00:00:00", + level: 30, + msg: "Ghost starts now.", + }), + ); }); }); - it('data.err', async function () { + it("data.err", async function () { await new Promise((resolve) => { - var ghostPrettyStream = new PrettyStream({mode: 'short'}); + var ghostPrettyStream = new PrettyStream({ mode: "short" }); var writeStream = new Writable(); writeStream._write = function (data) { data = data.toString(); - assert.equal(data, '[2016-07-01 00:00:00] \u001b[31mERROR\u001b[39m message\n\u001b[31m\n\u001b[31mHey Jude!\u001b[39m\n\n\u001b[1m\u001b[37mError Code: \u001b[39m\u001b[22m\n \u001b[90mHEY_JUDE\u001b[39m\n\n\u001b[90m----------------------------------------\u001b[39m\n\n\u001b[90mstack\u001b[39m\n\u001b[39m\n'); + assert.equal( + data, + "[2016-07-01 00:00:00] \u001b[31mERROR\u001b[39m message\n\u001b[31m\n\u001b[31mHey Jude!\u001b[39m\n\n\u001b[1m\u001b[37mError Code: \u001b[39m\u001b[22m\n \u001b[90mHEY_JUDE\u001b[39m\n\n\u001b[90m----------------------------------------\u001b[39m\n\n\u001b[90mstack\u001b[39m\n\u001b[39m\n", + ); resolve(); }; ghostPrettyStream.pipe(writeStream); - ghostPrettyStream.write(JSON.stringify({ - time: '2016-07-01 00:00:00', - level: 50, - msg: 'message', - err: { - message: 'Hey Jude!', - stack: 'stack', - code: 'HEY_JUDE' - } - })); + ghostPrettyStream.write( + JSON.stringify({ + time: "2016-07-01 00:00:00", + level: 50, + msg: "message", + err: { + message: "Hey Jude!", + stack: "stack", + code: "HEY_JUDE", + }, + }), + ); }); }); - it('data.req && data.res', async function () { + it("data.req && data.res", async function () { await new Promise((resolve) => { - var ghostPrettyStream = new PrettyStream({mode: 'short'}); + var ghostPrettyStream = new PrettyStream({ mode: "short" }); var writeStream = new Writable(); writeStream._write = function (data) { data = data.toString(); - assert.equal(data, '[2016-07-01 00:00:00] \u001b[36mINFO\u001b[39m "GET /test" \u001b[32m200\u001b[39m 39ms\n'); + assert.equal( + data, + '[2016-07-01 00:00:00] \u001b[36mINFO\u001b[39m "GET /test" \u001b[32m200\u001b[39m 39ms\n', + ); resolve(); }; ghostPrettyStream.pipe(writeStream); - ghostPrettyStream.write(JSON.stringify({ - time: '2016-07-01 00:00:00', - level: 30, - req: { - originalUrl: '/test', - method: 'GET', - body: { - a: 'b' - } - }, - res: { - statusCode: 200, - responseTime: '39ms' - } - })); + ghostPrettyStream.write( + JSON.stringify({ + time: "2016-07-01 00:00:00", + level: 30, + req: { + originalUrl: "/test", + method: "GET", + body: { + a: "b", + }, + }, + res: { + statusCode: 200, + responseTime: "39ms", + }, + }), + ); }); }); - it('data.req && data.res && data.err', async function () { + it("data.req && data.res && data.err", async function () { await new Promise((resolve) => { - var ghostPrettyStream = new PrettyStream({mode: 'short'}); + var ghostPrettyStream = new PrettyStream({ mode: "short" }); var writeStream = new Writable(); writeStream._write = function (data) { data = data.toString(); - assert.equal(data, '[2016-07-01 00:00:00] \u001b[31mERROR\u001b[39m "GET /test" \u001b[33m400\u001b[39m 39ms\n\u001b[31m\n\u001b[31mmessage\u001b[39m\n\n\u001b[90m----------------------------------------\u001b[39m\n\n\u001b[90mstack\u001b[39m\n\u001b[39m\n'); + assert.equal( + data, + '[2016-07-01 00:00:00] \u001b[31mERROR\u001b[39m "GET /test" \u001b[33m400\u001b[39m 39ms\n\u001b[31m\n\u001b[31mmessage\u001b[39m\n\n\u001b[90m----------------------------------------\u001b[39m\n\n\u001b[90mstack\u001b[39m\n\u001b[39m\n', + ); resolve(); }; ghostPrettyStream.pipe(writeStream); - ghostPrettyStream.write(JSON.stringify({ - time: '2016-07-01 00:00:00', - level: 50, - req: { - originalUrl: '/test', - method: 'GET', - body: { - a: 'b' - } - }, - res: { - statusCode: 400, - responseTime: '39ms' - }, - err: { - message: 'message', - stack: 'stack' - } - })); + ghostPrettyStream.write( + JSON.stringify({ + time: "2016-07-01 00:00:00", + level: 50, + req: { + originalUrl: "/test", + method: "GET", + body: { + a: "b", + }, + }, + res: { + statusCode: 400, + responseTime: "39ms", + }, + err: { + message: "message", + stack: "stack", + }, + }), + ); }); }); }); - describe('long mode', function () { - it('data.msg', async function () { + describe("long mode", function () { + it("data.msg", async function () { await new Promise((resolve) => { - var ghostPrettyStream = new PrettyStream({mode: 'long'}); + var ghostPrettyStream = new PrettyStream({ mode: "long" }); var writeStream = new Writable(); writeStream._write = function (data) { data = data.toString(); - assert.equal(data, '[2016-07-01 00:00:00] \u001b[36mINFO\u001b[39m Ghost starts now.\n'); + assert.equal( + data, + "[2016-07-01 00:00:00] \u001b[36mINFO\u001b[39m Ghost starts now.\n", + ); resolve(); }; ghostPrettyStream.pipe(writeStream); - ghostPrettyStream.write(JSON.stringify({ - time: '2016-07-01 00:00:00', - level: 30, - msg: 'Ghost starts now.' - })); + ghostPrettyStream.write( + JSON.stringify({ + time: "2016-07-01 00:00:00", + level: 30, + msg: "Ghost starts now.", + }), + ); }); }); - it('data.err', async function () { + it("data.err", async function () { await new Promise((resolve) => { - var ghostPrettyStream = new PrettyStream({mode: 'long'}); + var ghostPrettyStream = new PrettyStream({ mode: "long" }); var writeStream = new Writable(); writeStream._write = function (data) { data = data.toString(); - assert.equal(data, '[2016-07-01 00:00:00] \u001b[31mERROR\u001b[39m\n\u001b[31m\n\u001b[31mHey Jude!\u001b[39m\n\n\u001b[90m----------------------------------------\u001b[39m\n\n\u001b[90mstack\u001b[39m\n\u001b[39m\n\u001b[90m\u001b[39m\n'); + assert.equal( + data, + "[2016-07-01 00:00:00] \u001b[31mERROR\u001b[39m\n\u001b[31m\n\u001b[31mHey Jude!\u001b[39m\n\n\u001b[90m----------------------------------------\u001b[39m\n\n\u001b[90mstack\u001b[39m\n\u001b[39m\n\u001b[90m\u001b[39m\n", + ); resolve(); }; ghostPrettyStream.pipe(writeStream); - ghostPrettyStream.write(JSON.stringify({ - time: '2016-07-01 00:00:00', - level: 50, - err: { - message: 'Hey Jude!', - stack: 'stack' - } - })); + ghostPrettyStream.write( + JSON.stringify({ + time: "2016-07-01 00:00:00", + level: 50, + err: { + message: "Hey Jude!", + stack: "stack", + }, + }), + ); }); }); - it('data.req && data.res', async function () { + it("data.req && data.res", async function () { await new Promise((resolve) => { - var ghostPrettyStream = new PrettyStream({mode: 'long'}); + var ghostPrettyStream = new PrettyStream({ mode: "long" }); var writeStream = new Writable(); writeStream._write = function (data) { data = data.toString(); - assert.equal(data, '[2016-07-01 00:00:00] \u001b[36mINFO\u001b[39m "GET /test" \u001b[32m200\u001b[39m 39ms\n\u001b[90m\n\u001b[33mREQ\u001b[39m\n\u001b[32mip: \u001b[39m 127.0.01\n\u001b[32moriginalUrl: \u001b[39m/test\n\u001b[32mmethod: \u001b[39m GET\n\u001b[32mbody: \u001b[39m\n \u001b[32ma: \u001b[39mb\n\n\u001b[33mRES\u001b[39m\n\u001b[32mresponseTime: \u001b[39m39ms\n\u001b[39m\n'); + assert.equal( + data, + '[2016-07-01 00:00:00] \u001b[36mINFO\u001b[39m "GET /test" \u001b[32m200\u001b[39m 39ms\n\u001b[90m\n\u001b[33mREQ\u001b[39m\n\u001b[32mip: \u001b[39m 127.0.01\n\u001b[32moriginalUrl: \u001b[39m/test\n\u001b[32mmethod: \u001b[39m GET\n\u001b[32mbody: \u001b[39m\n \u001b[32ma: \u001b[39mb\n\n\u001b[33mRES\u001b[39m\n\u001b[32mresponseTime: \u001b[39m39ms\n\u001b[39m\n', + ); resolve(); }; ghostPrettyStream.pipe(writeStream); - ghostPrettyStream.write(JSON.stringify({ - time: '2016-07-01 00:00:00', - level: 30, - req: { - ip: '127.0.01', - originalUrl: '/test', - method: 'GET', - body: { - a: 'b' - } - }, - res: { - statusCode: 200, - responseTime: '39ms' - } - })); + ghostPrettyStream.write( + JSON.stringify({ + time: "2016-07-01 00:00:00", + level: 30, + req: { + ip: "127.0.01", + originalUrl: "/test", + method: "GET", + body: { + a: "b", + }, + }, + res: { + statusCode: 200, + responseTime: "39ms", + }, + }), + ); }); }); - it('data.req && data.res && data.err', async function () { + it("data.req && data.res && data.err", async function () { await new Promise((resolve) => { - var ghostPrettyStream = new PrettyStream({mode: 'long'}); + var ghostPrettyStream = new PrettyStream({ mode: "long" }); var writeStream = new Writable(); writeStream._write = function (data) { data = data.toString(); - assert.equal(data, '[2016-07-01 00:00:00] \u001b[31mERROR\u001b[39m "GET /test" \u001b[33m400\u001b[39m 39ms\n\u001b[31m\n\u001b[31mHey Jude!\u001b[39m\n\n\u001b[90m----------------------------------------\u001b[39m\n\n\u001b[90mstack\u001b[39m\n\u001b[39m\n\u001b[90m\n\u001b[33mREQ\u001b[39m\n\u001b[32moriginalUrl: \u001b[39m/test\n\u001b[32mmethod: \u001b[39m GET\n\u001b[32mbody: \u001b[39m\n \u001b[32ma: \u001b[39mb\n\n\u001b[33mRES\u001b[39m\n\u001b[32mresponseTime: \u001b[39m39ms\n\u001b[39m\n'); + assert.equal( + data, + '[2016-07-01 00:00:00] \u001b[31mERROR\u001b[39m "GET /test" \u001b[33m400\u001b[39m 39ms\n\u001b[31m\n\u001b[31mHey Jude!\u001b[39m\n\n\u001b[90m----------------------------------------\u001b[39m\n\n\u001b[90mstack\u001b[39m\n\u001b[39m\n\u001b[90m\n\u001b[33mREQ\u001b[39m\n\u001b[32moriginalUrl: \u001b[39m/test\n\u001b[32mmethod: \u001b[39m GET\n\u001b[32mbody: \u001b[39m\n \u001b[32ma: \u001b[39mb\n\n\u001b[33mRES\u001b[39m\n\u001b[32mresponseTime: \u001b[39m39ms\n\u001b[39m\n', + ); resolve(); }; ghostPrettyStream.pipe(writeStream); - ghostPrettyStream.write(JSON.stringify({ - time: '2016-07-01 00:00:00', - level: 50, - req: { - originalUrl: '/test', - method: 'GET', - body: { - a: 'b' - } - }, - res: { - statusCode: 400, - responseTime: '39ms' - }, - err: { - message: 'Hey Jude!', - stack: 'stack' - } - })); + ghostPrettyStream.write( + JSON.stringify({ + time: "2016-07-01 00:00:00", + level: 50, + req: { + originalUrl: "/test", + method: "GET", + body: { + a: "b", + }, + }, + res: { + statusCode: 400, + responseTime: "39ms", + }, + err: { + message: "Hey Jude!", + stack: "stack", + }, + }), + ); }); }); - it('data.err contains error details && meta fields', async function () { + it("data.err contains error details && meta fields", async function () { await new Promise((resolve) => { - var ghostPrettyStream = new PrettyStream({mode: 'long'}); + var ghostPrettyStream = new PrettyStream({ mode: "long" }); var writeStream = new Writable(); writeStream._write = function (data) { data = data.toString(); - assert.equal(data, '[2016-07-01 00:00:00] \u001b[31mERROR\u001b[39m\n\u001b[31m\n\u001b[31mType: ValidationError\u001b[39m\n\u001b[31mHey Jude!\u001b[39m\n\n\u001b[37m{"a":"b"}\u001b[39m\n\u001b[33mCheck documentation at https://docs.ghost.org/\u001b[39m\n\n\u001b[1m\u001b[37mError ID:\u001b[39m\u001b[22m\n \u001b[90me8546680-401f-11e9-99a7-ed7d6251b35c\u001b[39m\n\n\u001b[1m\u001b[37mDetails:\u001b[39m\u001b[22m\n\u001b[90m level: error\n rule: Templates must contain valid Handlebars.\n failures: \n - \n ref: default.hbs\n message: Missing helper: "image"\n code: GS005-TPL-ERR\u001b[39m\n\n\u001b[90m----------------------------------------\u001b[39m\n\n\u001b[90mstack\u001b[39m\n\u001b[39m\n\u001b[90m\u001b[39m\n'); + assert.equal( + data, + '[2016-07-01 00:00:00] \u001b[31mERROR\u001b[39m\n\u001b[31m\n\u001b[31mType: ValidationError\u001b[39m\n\u001b[31mHey Jude!\u001b[39m\n\n\u001b[37m{"a":"b"}\u001b[39m\n\u001b[33mCheck documentation at https://docs.ghost.org/\u001b[39m\n\n\u001b[1m\u001b[37mError ID:\u001b[39m\u001b[22m\n \u001b[90me8546680-401f-11e9-99a7-ed7d6251b35c\u001b[39m\n\n\u001b[1m\u001b[37mDetails:\u001b[39m\u001b[22m\n\u001b[90m level: error\n rule: Templates must contain valid Handlebars.\n failures: \n - \n ref: default.hbs\n message: Missing helper: "image"\n code: GS005-TPL-ERR\u001b[39m\n\n\u001b[90m----------------------------------------\u001b[39m\n\n\u001b[90mstack\u001b[39m\n\u001b[39m\n\u001b[90m\u001b[39m\n', + ); resolve(); }; ghostPrettyStream.pipe(writeStream); - ghostPrettyStream.write(JSON.stringify({ - time: '2016-07-01 00:00:00', - level: 50, - err: { - message: 'Hey Jude!', - stack: 'stack', - errorType: 'ValidationError', - id: 'e8546680-401f-11e9-99a7-ed7d6251b35c', - context: JSON.stringify({a: 'b'}), - help: 'Check documentation at https://docs.ghost.org/', - errorDetails: JSON.stringify([{ - level: 'error', - rule: 'Templates must contain valid Handlebars.', - failures: [{ref: 'default.hbs', message: 'Missing helper: "image"'}], - code: 'GS005-TPL-ERR' - }]) - } - })); + ghostPrettyStream.write( + JSON.stringify({ + time: "2016-07-01 00:00:00", + level: 50, + err: { + message: "Hey Jude!", + stack: "stack", + errorType: "ValidationError", + id: "e8546680-401f-11e9-99a7-ed7d6251b35c", + context: JSON.stringify({ a: "b" }), + help: "Check documentation at https://docs.ghost.org/", + errorDetails: JSON.stringify([ + { + level: "error", + rule: "Templates must contain valid Handlebars.", + failures: [ + { ref: "default.hbs", message: 'Missing helper: "image"' }, + ], + code: "GS005-TPL-ERR", + }, + ]), + }, + }), + ); }); }); - it('data with no time field', async function () { + it("data with no time field", async function () { await new Promise((resolve) => { - var ghostPrettyStream = new PrettyStream({mode: 'short'}); + var ghostPrettyStream = new PrettyStream({ mode: "short" }); var writeStream = new Writable(); // Hardcode the datetime so we don't have flaky tests - sinon.useFakeTimers(new Date('2024-12-15T13:17:00.000')); + sinon.useFakeTimers(new Date("2024-12-15T13:17:00.000")); writeStream._write = function (data) { data = data.toString(); - assert.equal(data, `[2024-12-15 13:17:00] \u001b[36mINFO\u001b[39m Ghost starts now.\n`); + assert.equal( + data, + `[2024-12-15 13:17:00] \u001b[36mINFO\u001b[39m Ghost starts now.\n`, + ); resolve(); }; ghostPrettyStream.pipe(writeStream); // Write the body with no time field - ghostPrettyStream.write(JSON.stringify({ - level: 30, - msg: 'Ghost starts now.' - })); + ghostPrettyStream.write( + JSON.stringify({ + level: 30, + msg: "Ghost starts now.", + }), + ); }); }); }); - describe('edge paths', function () { - it('defaults to short mode when no options are provided', function () { + describe("edge paths", function () { + it("defaults to short mode when no options are provided", function () { var ghostPrettyStream = new PrettyStream(); - assert.equal(ghostPrettyStream.mode, 'short'); + assert.equal(ghostPrettyStream.mode, "short"); }); - it('accepts plain object writes and stringifies internally', async function () { + it("accepts plain object writes and stringifies internally", async function () { await new Promise((resolve) => { - var ghostPrettyStream = new PrettyStream({mode: 'short'}); + var ghostPrettyStream = new PrettyStream({ mode: "short" }); var writeStream = new Writable(); writeStream._write = function (data) { data = data.toString(); - assert.equal(data.includes('Object input'), true); + assert.equal(data.includes("Object input"), true); resolve(); }; ghostPrettyStream.pipe(writeStream); ghostPrettyStream.write({ - time: '2016-07-01 00:00:00', + time: "2016-07-01 00:00:00", level: 30, - msg: 'Object input' + msg: "Object input", }); }); }); - it('handles invalid JSON input in _transform', async function () { + it("handles invalid JSON input in _transform", async function () { await new Promise((resolve) => { - var ghostPrettyStream = new PrettyStream({mode: 'short'}); - ghostPrettyStream._transform(Buffer.from('{not-json'), null, (err) => { + var ghostPrettyStream = new PrettyStream({ mode: "short" }); + ghostPrettyStream._transform(Buffer.from("{not-json"), null, (err) => { assert.notEqual(err, null); resolve(); }); }); }); - it('renders raw errorDetails when parsing details fails', async function () { + it("renders raw errorDetails when parsing details fails", async function () { await new Promise((resolve) => { - var ghostPrettyStream = new PrettyStream({mode: 'long'}); + var ghostPrettyStream = new PrettyStream({ mode: "long" }); var writeStream = new Writable(); writeStream._write = function (data) { data = data.toString(); - assert.equal(data.includes('Details:'), true); - assert.equal(data.includes('not-json-details'), true); + assert.equal(data.includes("Details:"), true); + assert.equal(data.includes("not-json-details"), true); resolve(); }; ghostPrettyStream.pipe(writeStream); - ghostPrettyStream.write(JSON.stringify({ - time: '2016-07-01 00:00:00', - level: 50, - err: { - message: 'oops', - stack: 'stack', - errorDetails: 'not-json-details' - } - })); + ghostPrettyStream.write( + JSON.stringify({ + time: "2016-07-01 00:00:00", + level: 50, + err: { + message: "oops", + stack: "stack", + errorDetails: "not-json-details", + }, + }), + ); }); }); - it('renders parsed object errorDetails payloads', async function () { + it("renders parsed object errorDetails payloads", async function () { await new Promise((resolve) => { - var ghostPrettyStream = new PrettyStream({mode: 'long'}); + var ghostPrettyStream = new PrettyStream({ mode: "long" }); var writeStream = new Writable(); writeStream._write = function (data) { data = data.toString(); - assert.equal(data.includes('Details:'), true); - assert.equal(data.includes('CODE1'), true); + assert.equal(data.includes("Details:"), true); + assert.equal(data.includes("CODE1"), true); resolve(); }; ghostPrettyStream.pipe(writeStream); - ghostPrettyStream.write(JSON.stringify({ - time: '2016-07-01 00:00:00', - level: 50, - err: { - message: 'oops', - stack: 'stack', - errorDetails: JSON.stringify({code: 'CODE1'}) - } - })); + ghostPrettyStream.write( + JSON.stringify({ + time: "2016-07-01 00:00:00", + level: 50, + err: { + message: "oops", + stack: "stack", + errorDetails: JSON.stringify({ code: "CODE1" }), + }, + }), + ); }); }); - it('renders non-object additional fields', async function () { + it("renders non-object additional fields", async function () { await new Promise((resolve) => { - var ghostPrettyStream = new PrettyStream({mode: 'long'}); + var ghostPrettyStream = new PrettyStream({ mode: "long" }); var writeStream = new Writable(); writeStream._write = function (data) { data = data.toString(); - assert.equal(data.includes('plain-extra-value'), true); + assert.equal(data.includes("plain-extra-value"), true); resolve(); }; ghostPrettyStream.pipe(writeStream); - ghostPrettyStream.write(JSON.stringify({ - time: '2016-07-01 00:00:00', - level: 30, - msg: 'hello', - extra: 'plain-extra-value' - })); + ghostPrettyStream.write( + JSON.stringify({ + time: "2016-07-01 00:00:00", + level: 30, + msg: "hello", + extra: "plain-extra-value", + }), + ); }); }); - it('colors 500 status code as red', async function () { + it("colors 500 status code as red", async function () { await new Promise((resolve) => { - var ghostPrettyStream = new PrettyStream({mode: 'short'}); + var ghostPrettyStream = new PrettyStream({ mode: "short" }); var writeStream = new Writable(); writeStream._write = function (data) { data = data.toString(); - assert.equal(data.includes('\u001b[31m500\u001b[39m'), true); + assert.equal(data.includes("\u001b[31m500\u001b[39m"), true); resolve(); }; ghostPrettyStream.pipe(writeStream); - ghostPrettyStream.write(JSON.stringify({ - time: '2016-07-01 00:00:00', - level: 30, - req: {originalUrl: '/a', method: 'GET'}, - res: {statusCode: 500, responseTime: '1ms'} - })); + ghostPrettyStream.write( + JSON.stringify({ + time: "2016-07-01 00:00:00", + level: 30, + req: { originalUrl: "/a", method: "GET" }, + res: { statusCode: 500, responseTime: "1ms" }, + }), + ); }); }); - it('colors 301 status code as cyan', async function () { + it("colors 301 status code as cyan", async function () { await new Promise((resolve) => { - var ghostPrettyStream = new PrettyStream({mode: 'short'}); + var ghostPrettyStream = new PrettyStream({ mode: "short" }); var writeStream = new Writable(); writeStream._write = function (data) { data = data.toString(); - assert.equal(data.includes('\u001b[36m301\u001b[39m'), true); + assert.equal(data.includes("\u001b[36m301\u001b[39m"), true); resolve(); }; ghostPrettyStream.pipe(writeStream); - ghostPrettyStream.write(JSON.stringify({ - time: '2016-07-01 00:00:00', - level: 30, - req: {originalUrl: '/b', method: 'GET'}, - res: {statusCode: 301, responseTime: '1ms'} - })); + ghostPrettyStream.write( + JSON.stringify({ + time: "2016-07-01 00:00:00", + level: 30, + req: { originalUrl: "/b", method: "GET" }, + res: { statusCode: 301, responseTime: "1ms" }, + }), + ); }); }); - it('colors <200 status code with default color', async function () { + it("colors <200 status code with default color", async function () { await new Promise((resolve) => { - var ghostPrettyStream = new PrettyStream({mode: 'short'}); + var ghostPrettyStream = new PrettyStream({ mode: "short" }); var writeStream = new Writable(); writeStream._write = function (data) { data = data.toString(); - assert.equal(data.includes('\u001b[39m100\u001b[39m'), true); + assert.equal(data.includes("\u001b[39m100\u001b[39m"), true); resolve(); }; ghostPrettyStream.pipe(writeStream); - ghostPrettyStream.write(JSON.stringify({ - time: '2016-07-01 00:00:00', - level: 30, - req: {originalUrl: '/c', method: 'GET'}, - res: {statusCode: 100, responseTime: '1ms'} - })); + ghostPrettyStream.write( + JSON.stringify({ + time: "2016-07-01 00:00:00", + level: 30, + req: { originalUrl: "/c", method: "GET" }, + res: { statusCode: 100, responseTime: "1ms" }, + }), + ); }); }); }); diff --git a/packages/prometheus-metrics/.eslintrc.js b/packages/prometheus-metrics/.eslintrc.js index ecc28524e..0adb06575 100644 --- a/packages/prometheus-metrics/.eslintrc.js +++ b/packages/prometheus-metrics/.eslintrc.js @@ -1,7 +1,5 @@ module.exports = { - parser: '@typescript-eslint/parser', - plugins: ['ghost'], - extends: [ - 'plugin:ghost/node' - ] + parser: "@typescript-eslint/parser", + plugins: ["ghost"], + extends: ["plugin:ghost/node"], }; diff --git a/packages/prometheus-metrics/README.md b/packages/prometheus-metrics/README.md index 98745fef3..9397a6c81 100644 --- a/packages/prometheus-metrics/README.md +++ b/packages/prometheus-metrics/README.md @@ -2,26 +2,22 @@ A standalone server for exporting prometheus metrics from Ghost - ## Purpose Prometheus metrics integration for exposing Ghost runtime counters, gauges, and histograms. ## Usage - ## Develop This is a monorepo package. Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests - diff --git a/packages/prometheus-metrics/package.json b/packages/prometheus-metrics/package.json index 6719af3b4..9743feb7b 100644 --- a/packages/prometheus-metrics/package.json +++ b/packages/prometheus-metrics/package.json @@ -1,17 +1,20 @@ { "name": "@tryghost/prometheus-metrics", "version": "3.0.3", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/prometheus-metrics" }, - "author": "Ghost Foundation", + "files": [ + "build" + ], + "main": "build/index.js", + "types": "build/index.d.ts", "publishConfig": { "access": "public" }, - "main": "build/index.js", - "types": "build/index.d.ts", "scripts": { "dev": "tsc --watch --preserveWatchOutput --sourceMap", "build": "yarn build:ts", @@ -25,9 +28,12 @@ "lint:test": "eslint -c test/.eslintrc.js test/ --ext .ts --cache", "lint:eslint": "yarn lint:code && yarn lint:test" }, - "files": [ - "build" - ], + "dependencies": { + "@tryghost/logging": "^4.0.3", + "express": "5.2.1", + "prom-client": "15.1.3", + "stoppable": "1.1.0" + }, "devDependencies": { "@types/express": "5.0.6", "@types/sinon": "21.0.0", @@ -39,11 +45,5 @@ "supertest": "7.2.2", "ts-node": "10.9.2", "typescript": "5.9.3" - }, - "dependencies": { - "@tryghost/logging": "^4.0.3", - "express": "5.2.1", - "prom-client": "15.1.3", - "stoppable": "1.1.0" } } diff --git a/packages/prometheus-metrics/src/MetricsServer.ts b/packages/prometheus-metrics/src/MetricsServer.ts index d497e424e..0e5e186da 100644 --- a/packages/prometheus-metrics/src/MetricsServer.ts +++ b/packages/prometheus-metrics/src/MetricsServer.ts @@ -1,8 +1,8 @@ -import debugModule from '@tryghost/debug'; -import express from 'express'; -import stoppable from 'stoppable'; +import debugModule from "@tryghost/debug"; +import express from "express"; +import stoppable from "stoppable"; -const debug = debugModule('metrics-server'); +const debug = debugModule("metrics-server"); type ServerConfig = { host: string; @@ -11,8 +11,8 @@ type ServerConfig = { type CreateApp = () => express.Application; type CreateStoppableServer = ( - server: ReturnType, - grace?: number + server: ReturnType, + grace?: number, ) => stoppable.StoppableServer; export class MetricsServer { @@ -28,7 +28,7 @@ export class MetricsServer { serverConfig, handler, createApp, - createStoppableServer + createStoppableServer, }: { serverConfig: ServerConfig; handler: express.Handler; @@ -45,21 +45,23 @@ export class MetricsServer { } async start() { - debug('Starting metrics server'); + debug("Starting metrics server"); this.app = this.createApp(); - this.app.get('/metrics', this.handler); + this.app.get("/metrics", this.handler); const httpServer = this.app.listen(this.serverConfig.port, this.serverConfig.host, () => { - debug(`Metrics server listening at ${this.serverConfig.host}:${this.serverConfig.port}`); + debug( + `Metrics server listening at ${this.serverConfig.host}:${this.serverConfig.port}`, + ); }); this.httpServer = this.createStoppableServer(httpServer, 0); - process.on('SIGINT', () => this.shutdown()); - process.on('SIGTERM', () => this.shutdown()); - return {app: this.app, httpServer: this.httpServer}; + process.on("SIGINT", () => this.shutdown()); + process.on("SIGTERM", () => this.shutdown()); + return { app: this.app, httpServer: this.httpServer }; } async stop() { - debug('Stopping metrics server'); + debug("Stopping metrics server"); if (this.httpServer && this.httpServer.listening) { await this.httpServer.stop(); } diff --git a/packages/prometheus-metrics/src/PrometheusClient.ts b/packages/prometheus-metrics/src/PrometheusClient.ts index 57f83a31f..34e7ea3e0 100644 --- a/packages/prometheus-metrics/src/PrometheusClient.ts +++ b/packages/prometheus-metrics/src/PrometheusClient.ts @@ -1,9 +1,9 @@ -import {Request, Response} from 'express'; -import http from 'http'; -import client from 'prom-client'; -import type {Metric, MetricObjectWithValues, MetricValue} from 'prom-client'; -import type {Knex} from 'knex'; -import logging from '@tryghost/logging'; +import { Request, Response } from "express"; +import http from "http"; +import client from "prom-client"; +import type { Metric, MetricObjectWithValues, MetricValue } from "prom-client"; +import type { Knex } from "knex"; +import logging from "@tryghost/logging"; type PrometheusClientConfig = { register?: client.Registry; @@ -12,7 +12,7 @@ type PrometheusClientConfig = { url?: string; interval?: number; jobName?: string; - } + }; }; /** @@ -26,7 +26,7 @@ export class PrometheusClient { constructor(prometheusConfig: PrometheusClientConfig = {}, logger: any = logging) { this.config = prometheusConfig; this.client = client; - this.prefix = 'ghost_'; + this.prefix = "ghost_"; this.logger = logger; } @@ -48,14 +48,14 @@ export class PrometheusClient { init() { this.collectDefaultMetrics(); if (this.config.pushgateway?.enabled) { - const gatewayUrl = this.config.pushgateway.url || 'http://localhost:9091'; + const gatewayUrl = this.config.pushgateway.url || "http://localhost:9091"; const interval = this.config.pushgateway.interval || 15000; this.gateway = new client.Pushgateway(gatewayUrl, { timeout: 5000, agent: new http.Agent({ keepAlive: true, - maxSockets: 1 - }) + maxSockets: 1, + }), }); this.pushMetrics(); this.pushInterval = setInterval(() => { @@ -69,22 +69,22 @@ export class PrometheusClient { */ async pushMetrics() { if (this.pushRetries >= 3) { - this.logger.error('Failed to push metrics to pushgateway 3 times in a row, giving up'); + this.logger.error("Failed to push metrics to pushgateway 3 times in a row, giving up"); this.stop(); return; } if (this.config.pushgateway?.enabled && this.gateway) { - const jobName = this.config.pushgateway?.jobName || 'ghost'; + const jobName = this.config.pushgateway?.jobName || "ghost"; try { - await this.gateway.pushAdd({jobName}); - this.logger.debug('Metrics pushed to pushgateway - jobName: ', jobName); + await this.gateway.pushAdd({ jobName }); + this.logger.debug("Metrics pushed to pushgateway - jobName: ", jobName); this.pushRetries = 0; } catch (err) { let error; - if (typeof err === 'object' && err !== null && 'code' in err) { - error = 'Error pushing metrics to pushgateway: ' + err.code as string; + if (typeof err === "object" && err !== null && "code" in err) { + error = ("Error pushing metrics to pushgateway: " + err.code) as string; } else { - error = 'Error pushing metrics to pushgateway: Unknown error'; + error = "Error pushing metrics to pushgateway: Unknown error"; } this.logger.error(error); this.pushRetries = this.pushRetries + 1; @@ -107,7 +107,7 @@ export class PrometheusClient { * Only called once on init */ collectDefaultMetrics() { - this.client.collectDefaultMetrics({prefix: this.prefix}); + this.client.collectDefaultMetrics({ prefix: this.prefix }); } /** @@ -117,13 +117,13 @@ export class PrometheusClient { */ async handleMetricsRequest(req: Request, res: Response) { try { - res.set('Content-Type', this.getContentType()); + res.set("Content-Type", this.getContentType()); res.end(await this.getMetrics()); } catch (err) { if (err instanceof Error && err.message) { res.status(500).end(err.message); } else { - res.status(500).end('Unknown error'); + res.status(500).end("Unknown error"); } } } @@ -137,7 +137,7 @@ export class PrometheusClient { /** * Returns the metrics from the registry as a JSON object - * + * * Particularly useful for testing */ async getMetricsAsJSON(): Promise { @@ -172,7 +172,9 @@ export class PrometheusClient { * @param name - The name of the metric * @returns The values of the metric */ - async getMetricObject(name: string): Promise> | undefined> { + async getMetricObject( + name: string, + ): Promise> | undefined> { const metric = this.getMetric(name); if (!metric) { return undefined; @@ -189,7 +191,7 @@ export class PrometheusClient { } /** - * + * */ /** @@ -199,11 +201,19 @@ export class PrometheusClient { * @param labelNames - The names of the labels for the metric * @returns The counter metric */ - registerCounter({name, help, labelNames = []}: {name: string, help: string, labelNames?: string[]}): client.Counter { + registerCounter({ + name, + help, + labelNames = [], + }: { + name: string; + help: string; + labelNames?: string[]; + }): client.Counter { return new this.client.Counter({ name: `${this.prefix}${name}`, help, - labelNames + labelNames, }); } @@ -214,11 +224,19 @@ export class PrometheusClient { * @param collect - The collect function to use for the gauge * @returns The gauge metric */ - registerGauge({name, help, collect}: {name: string, help: string, collect?: () => void}): client.Gauge { + registerGauge({ + name, + help, + collect, + }: { + name: string; + help: string; + collect?: () => void; + }): client.Gauge { return new this.client.Gauge({ name: `${this.prefix}${name}`, help, - collect + collect, }); } @@ -230,7 +248,23 @@ export class PrometheusClient { * @param collect - The collect function to use for the summary * @returns The summary metric */ - registerSummary({name, help, percentiles, maxAgeSeconds, ageBuckets, pruneAgedBuckets, collect}: {name: string, help: string, percentiles?: number[], maxAgeSeconds?: number, ageBuckets?: number, pruneAgedBuckets?: boolean, collect?: () => void}): client.Summary { + registerSummary({ + name, + help, + percentiles, + maxAgeSeconds, + ageBuckets, + pruneAgedBuckets, + collect, + }: { + name: string; + help: string; + percentiles?: number[]; + maxAgeSeconds?: number; + ageBuckets?: number; + pruneAgedBuckets?: boolean; + collect?: () => void; + }): client.Summary { return new this.client.Summary({ name: `${this.prefix}${name}`, help, @@ -238,7 +272,7 @@ export class PrometheusClient { maxAgeSeconds, ageBuckets, pruneAgedBuckets, - collect + collect, }); } @@ -250,11 +284,20 @@ export class PrometheusClient { * @param collect - The collect function to use for the histogram * @returns The histogram metric */ - registerHistogram({name, help, buckets}: {name: string, help: string, buckets: number[], collect?: () => void}): client.Histogram { + registerHistogram({ + name, + help, + buckets, + }: { + name: string; + help: string; + buckets: number[]; + collect?: () => void; + }): client.Histogram { return new this.client.Histogram({ name: `${this.prefix}${name}`, help, - buckets: buckets + buckets: buckets, }); } @@ -267,122 +310,126 @@ export class PrometheusClient { instrumentKnex(knexInstance: Knex) { // Create some gauges for tracking the connection pool this.registerGauge({ - name: `db_connection_pool_max`, - help: 'The maximum number of connections allowed in the pool', + name: `db_connection_pool_max`, + help: "The maximum number of connections allowed in the pool", collect() { (this as unknown as client.Gauge).set(knexInstance.client.pool.max); - } + }, }); this.registerGauge({ - name: `db_connection_pool_min`, - help: 'The minimum number of connections allowed in the pool', + name: `db_connection_pool_min`, + help: "The minimum number of connections allowed in the pool", collect() { (this as unknown as client.Gauge).set(knexInstance.client.pool.min); - } + }, }); this.registerGauge({ - name: `db_connection_pool_active`, - help: 'The number of active connections to the database, which can be in use or idle', + name: `db_connection_pool_active`, + help: "The number of active connections to the database, which can be in use or idle", collect() { - (this as unknown as client.Gauge).set(knexInstance.client.pool.numUsed() + knexInstance.client.pool.numFree()); - } + (this as unknown as client.Gauge).set( + knexInstance.client.pool.numUsed() + knexInstance.client.pool.numFree(), + ); + }, }); this.registerGauge({ name: `db_connection_pool_used`, - help: 'The number of connections currently in use by the database', + help: "The number of connections currently in use by the database", collect() { (this as unknown as client.Gauge).set(knexInstance.client.pool.numUsed()); - } + }, }); this.registerGauge({ name: `db_connection_pool_idle`, - help: 'The number of active connections currently idle in pool', + help: "The number of active connections currently idle in pool", collect() { (this as unknown as client.Gauge).set(knexInstance.client.pool.numFree()); - } + }, }); this.registerGauge({ name: `db_connection_pool_pending_acquires`, - help: 'The number of connections currently waiting to be acquired from the pool', + help: "The number of connections currently waiting to be acquired from the pool", collect() { - (this as unknown as client.Gauge).set(knexInstance.client.pool.numPendingAcquires()); - } + (this as unknown as client.Gauge).set( + knexInstance.client.pool.numPendingAcquires(), + ); + }, }); this.registerGauge({ name: `db_connection_pool_pending_creates`, - help: 'The number of connections currently waiting to be created', + help: "The number of connections currently waiting to be created", collect() { (this as unknown as client.Gauge).set(knexInstance.client.pool.numPendingCreates()); - } + }, }); const queryDurationSummary = this.registerSummary({ name: `db_query_duration_seconds`, - help: 'Summary of the duration of knex database queries in seconds', + help: "Summary of the duration of knex database queries in seconds", percentiles: [0.5, 0.9, 0.99], maxAgeSeconds: 60, ageBuckets: 6, - pruneAgedBuckets: false + pruneAgedBuckets: false, }); const acquireDurationSummary = this.registerSummary({ name: `db_connection_acquire_duration_seconds`, - help: 'Summary of the duration of acquiring a connection from the pool in seconds', + help: "Summary of the duration of acquiring a connection from the pool in seconds", percentiles: [0.5, 0.9, 0.99], maxAgeSeconds: 60, ageBuckets: 6, - pruneAgedBuckets: false + pruneAgedBuckets: false, }); const createDurationSummary = this.registerSummary({ name: `db_connection_create_duration_seconds`, - help: 'Summary of the duration of creating a connection in seconds', + help: "Summary of the duration of creating a connection in seconds", percentiles: [0.5, 0.9, 0.99], maxAgeSeconds: 60, ageBuckets: 6, - pruneAgedBuckets: false + pruneAgedBuckets: false, }); - knexInstance.on('query', (query) => { + knexInstance.on("query", (query) => { // Add the query to the map this.queries.set(query.__knexQueryUid, queryDurationSummary.startTimer()); }); - knexInstance.on('query-response', (err, query) => { + knexInstance.on("query-response", (err, query) => { this.queries.get(query.__knexQueryUid)?.(); this.queries.delete(query.__knexQueryUid); }); - knexInstance.client.pool.on('createRequest', (eventId: number) => { + knexInstance.client.pool.on("createRequest", (eventId: number) => { this.creates.set(eventId, createDurationSummary.startTimer()); }); - knexInstance.client.pool.on('createSuccess', (eventId: number) => { + knexInstance.client.pool.on("createSuccess", (eventId: number) => { this.creates.get(eventId)?.(); this.creates.delete(eventId); }); - knexInstance.client.pool.on('createFail', (eventId: number) => { + knexInstance.client.pool.on("createFail", (eventId: number) => { this.creates.get(eventId)?.(); this.creates.delete(eventId); }); - knexInstance.client.pool.on('acquireRequest', (eventId: number) => { + knexInstance.client.pool.on("acquireRequest", (eventId: number) => { this.acquires.set(eventId, acquireDurationSummary.startTimer()); }); - knexInstance.client.pool.on('acquireSuccess', (eventId: number) => { + knexInstance.client.pool.on("acquireSuccess", (eventId: number) => { this.acquires.get(eventId)?.(); this.acquires.delete(eventId); }); - knexInstance.client.pool.on('acquireFail', (eventId: number) => { + knexInstance.client.pool.on("acquireFail", (eventId: number) => { this.acquires.get(eventId)?.(); this.acquires.delete(eventId); }); diff --git a/packages/prometheus-metrics/src/index.ts b/packages/prometheus-metrics/src/index.ts index 077ee8e12..3fd6026e6 100644 --- a/packages/prometheus-metrics/src/index.ts +++ b/packages/prometheus-metrics/src/index.ts @@ -1,2 +1,2 @@ -export * from './MetricsServer'; -export * from './PrometheusClient'; +export * from "./MetricsServer"; +export * from "./PrometheusClient"; diff --git a/packages/prometheus-metrics/src/libraries.d.ts b/packages/prometheus-metrics/src/libraries.d.ts index 98ac778dc..beb3198a9 100644 --- a/packages/prometheus-metrics/src/libraries.d.ts +++ b/packages/prometheus-metrics/src/libraries.d.ts @@ -1,2 +1,2 @@ -declare module '@tryghost/debug'; -declare module '@tryghost/logging'; \ No newline at end of file +declare module "@tryghost/debug"; +declare module "@tryghost/logging"; diff --git a/packages/prometheus-metrics/test/.eslintrc.js b/packages/prometheus-metrics/test/.eslintrc.js index 023956a15..47a9e966e 100644 --- a/packages/prometheus-metrics/test/.eslintrc.js +++ b/packages/prometheus-metrics/test/.eslintrc.js @@ -1,10 +1,8 @@ module.exports = { - parser: '@typescript-eslint/parser', - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ], + parser: "@typescript-eslint/parser", + plugins: ["ghost"], + extends: ["plugin:ghost/test"], globals: { - afterAll: 'readonly' - } + afterAll: "readonly", + }, }; diff --git a/packages/prometheus-metrics/test/metrics-server.test.ts b/packages/prometheus-metrics/test/metrics-server.test.ts index a9da243be..b7bebcf84 100644 --- a/packages/prometheus-metrics/test/metrics-server.test.ts +++ b/packages/prometheus-metrics/test/metrics-server.test.ts @@ -1,7 +1,7 @@ -import assert from 'assert/strict'; -import {MetricsServer} from '../src'; -import express from 'express'; -import * as sinon from 'sinon'; +import assert from "assert/strict"; +import { MetricsServer } from "../src"; +import express from "express"; +import * as sinon from "sinon"; type FakeApp = { get: sinon.SinonStub; @@ -13,14 +13,14 @@ type FakeStoppableServer = { stop: sinon.SinonStub; }; -describe('Metrics Server', function () { +describe("Metrics Server", function () { let metricsServer: MetricsServer; let serverConfig = { - host: '127.0.0.1', - port: 9416 + host: "127.0.0.1", + port: 9416, }; let handler = (req: express.Request, res: express.Response) => { - res.send('metrics'); + res.send("metrics"); }; let fakeApp: FakeApp; @@ -33,7 +33,7 @@ describe('Metrics Server', function () { fakeHttpServer = {}; fakeApp = { get: sinon.stub(), - listen: sinon.stub() + listen: sinon.stub(), }; fakeApp.listen.callsFake((port: number, host: string, cb?: () => void) => { cb?.(); @@ -42,7 +42,7 @@ describe('Metrics Server', function () { fakeStoppableServer = { listening: true, - stop: sinon.stub().resolves() + stop: sinon.stub().resolves(), }; createAppStub = sinon.stub().returns(fakeApp); @@ -52,7 +52,7 @@ describe('Metrics Server', function () { serverConfig, handler, createApp: createAppStub, - createStoppableServer: createStoppableServerStub + createStoppableServer: createStoppableServerStub, }); }); @@ -65,41 +65,46 @@ describe('Metrics Server', function () { await metricsServer.shutdown(); }); - describe('constructor', function () { - it('should create a new instance', function () { + describe("constructor", function () { + it("should create a new instance", function () { assert.ok(metricsServer); }); - it('should support default server factories', function () { - const instance = new MetricsServer({serverConfig, handler}); + it("should support default server factories", function () { + const instance = new MetricsServer({ serverConfig, handler }); assert.ok(instance); }); }); - describe('start', function () { - it('should start the server', async function () { + describe("start", function () { + it("should start the server", async function () { const server = await metricsServer.start(); assert.ok(server); sinon.assert.calledOnce(createAppStub); - sinon.assert.calledOnceWithExactly(fakeApp.get, '/metrics', handler); - sinon.assert.calledOnceWithExactly(fakeApp.listen, serverConfig.port, serverConfig.host, sinon.match.func); + sinon.assert.calledOnceWithExactly(fakeApp.get, "/metrics", handler); + sinon.assert.calledOnceWithExactly( + fakeApp.listen, + serverConfig.port, + serverConfig.host, + sinon.match.func, + ); sinon.assert.calledOnceWithExactly(createStoppableServerStub, fakeHttpServer, 0); }); - it('should use the provided handler', async function () { - const {app} = await metricsServer.start(); + it("should use the provided handler", async function () { + const { app } = await metricsServer.start(); assert.equal(app, fakeApp as unknown as express.Application); }); - it('should register shutdown handlers for SIGINT and SIGTERM', async function () { - const processOnStub = sinon.stub(process, 'on'); - const shutdownStub = sinon.stub(metricsServer, 'shutdown').resolves(); + it("should register shutdown handlers for SIGINT and SIGTERM", async function () { + const processOnStub = sinon.stub(process, "on"); + const shutdownStub = sinon.stub(metricsServer, "shutdown").resolves(); await metricsServer.start(); sinon.assert.calledTwice(processOnStub); - assert.equal(processOnStub.firstCall.args[0], 'SIGINT'); - assert.equal(processOnStub.secondCall.args[0], 'SIGTERM'); + assert.equal(processOnStub.firstCall.args[0], "SIGINT"); + assert.equal(processOnStub.secondCall.args[0], "SIGTERM"); await processOnStub.firstCall.args[1](); await processOnStub.secondCall.args[1](); @@ -107,15 +112,15 @@ describe('Metrics Server', function () { }); }); - describe('stop', function () { - it('should stop the server', async function () { + describe("stop", function () { + it("should stop the server", async function () { const server = await metricsServer.start(); await metricsServer.stop(); assert.ok(server); sinon.assert.calledOnce(fakeStoppableServer.stop); }); - it('should not stop when server is not listening', async function () { + it("should not stop when server is not listening", async function () { await metricsServer.start(); fakeStoppableServer.listening = false; await metricsServer.stop(); @@ -123,16 +128,16 @@ describe('Metrics Server', function () { }); }); - describe('shutdown', function () { - it('should shutdown the server', async function () { + describe("shutdown", function () { + it("should shutdown the server", async function () { const server = await metricsServer.start(); await metricsServer.shutdown(); assert.ok(server); sinon.assert.calledOnce(fakeStoppableServer.stop); }); - it('should not shutdown the server if it is already shutting down', async function () { - const stopSpy = sinon.spy(metricsServer, 'stop'); + it("should not shutdown the server if it is already shutting down", async function () { + const stopSpy = sinon.spy(metricsServer, "stop"); await metricsServer.start(); await Promise.all([metricsServer.shutdown(), metricsServer.shutdown()]); sinon.assert.calledOnce(stopSpy); diff --git a/packages/prometheus-metrics/test/prometheus-client.test.ts b/packages/prometheus-metrics/test/prometheus-client.test.ts index 10efedef6..6cb71006f 100644 --- a/packages/prometheus-metrics/test/prometheus-client.test.ts +++ b/packages/prometheus-metrics/test/prometheus-client.test.ts @@ -1,14 +1,14 @@ -import assert from 'assert/strict'; -import {PrometheusClient} from '../src'; -import {Request, Response} from 'express'; -import * as sinon from 'sinon'; -import type {Knex} from 'knex'; -import nock from 'nock'; -import {EventEmitter} from 'events'; -import type {EventEmitter as EventEmitterType} from 'events'; -import type {Gauge, Summary, Pushgateway, RegistryContentType, Metric} from 'prom-client'; - -describe('Prometheus Client', function () { +import assert from "assert/strict"; +import { PrometheusClient } from "../src"; +import { Request, Response } from "express"; +import * as sinon from "sinon"; +import type { Knex } from "knex"; +import nock from "nock"; +import { EventEmitter } from "events"; +import type { EventEmitter as EventEmitterType } from "events"; +import type { Gauge, Summary, Pushgateway, RegistryContentType, Metric } from "prom-client"; + +describe("Prometheus Client", function () { let instance: PrometheusClient; let logger: any; @@ -17,7 +17,7 @@ describe('Prometheus Client', function () { logger = { info: sinon.stub(), error: sinon.stub(), - debug: sinon.stub() + debug: sinon.stub(), }; }); @@ -29,84 +29,91 @@ describe('Prometheus Client', function () { nock.cleanAll(); }); - describe('constructor', function () { - it('should create a new instance', function () { + describe("constructor", function () { + it("should create a new instance", function () { instance = new PrometheusClient(); assert.ok(instance); }); }); - describe('init', function () { - it('should call collectDefaultMetrics', function () { + describe("init", function () { + it("should call collectDefaultMetrics", function () { instance = new PrometheusClient(); - const collectDefaultMetricsSpy = sinon.spy(instance.client, 'collectDefaultMetrics'); + const collectDefaultMetricsSpy = sinon.spy(instance.client, "collectDefaultMetrics"); instance.init(); assert.ok(collectDefaultMetricsSpy.called); }); - it('should create the pushgateway client if the pushgateway is enabled', async function () { + it("should create the pushgateway client if the pushgateway is enabled", async function () { const clock = sinon.useFakeTimers(); - nock('http://localhost:9091') - .persist() - .post('/metrics/job/ghost-test') - .reply(200); - - instance = new PrometheusClient({pushgateway: {enabled: true, interval: 20, jobName: 'ghost-test'}}); - const pushMetricsStub = sinon.stub(instance, 'pushMetrics').resolves(); + nock("http://localhost:9091").persist().post("/metrics/job/ghost-test").reply(200); + + instance = new PrometheusClient({ + pushgateway: { enabled: true, interval: 20, jobName: "ghost-test" }, + }); + const pushMetricsStub = sinon.stub(instance, "pushMetrics").resolves(); instance.init(); assert.ok(instance.gateway); - assert.ok(pushMetricsStub.called, 'pushMetrics should be called immediately'); + assert.ok(pushMetricsStub.called, "pushMetrics should be called immediately"); clock.tick(30); - assert.ok(pushMetricsStub.calledTwice, 'pushMetrics should be called again after the interval'); + assert.ok( + pushMetricsStub.calledTwice, + "pushMetrics should be called again after the interval", + ); clock.restore(); }); - it('should not create the pushgateway client if the pushgateway is disabled', function () { - instance = new PrometheusClient({pushgateway: {enabled: false}}); + it("should not create the pushgateway client if the pushgateway is disabled", function () { + instance = new PrometheusClient({ pushgateway: { enabled: false } }); instance.init(); assert.equal(instance.gateway, undefined); }); }); - describe('collectDefaultMetrics', function () { - it('should call collectDefaultMetrics on the client', function () { + describe("collectDefaultMetrics", function () { + it("should call collectDefaultMetrics on the client", function () { instance = new PrometheusClient(); - const collectDefaultMetricsSpy = sinon.spy(instance.client, 'collectDefaultMetrics'); + const collectDefaultMetricsSpy = sinon.spy(instance.client, "collectDefaultMetrics"); instance.collectDefaultMetrics(); assert.ok(collectDefaultMetricsSpy.called); }); }); - describe('pushMetrics', function () { - it('should use the default job name when one is not configured', async function () { + describe("pushMetrics", function () { + it("should use the default job name when one is not configured", async function () { const pushAddStub = sinon.stub().resolves(); - instance = new PrometheusClient({pushgateway: {enabled: true}}, logger); + instance = new PrometheusClient({ pushgateway: { enabled: true } }, logger); instance.gateway = { - pushAdd: pushAddStub + pushAdd: pushAddStub, } as unknown as Pushgateway; await instance.pushMetrics(); - assert.ok(pushAddStub.calledWith({jobName: 'ghost'})); + assert.ok(pushAddStub.calledWith({ jobName: "ghost" })); assert.ok(logger.debug.called); }); - it('should push metrics to the pushgateway', async function () { - const scope = nock('http://localhost:9091') + it("should push metrics to the pushgateway", async function () { + const scope = nock("http://localhost:9091") .persist() - .post('/metrics/job/ghost-test') + .post("/metrics/job/ghost-test") .reply(200); - instance = new PrometheusClient({pushgateway: {enabled: true, jobName: 'ghost-test'}}); + instance = new PrometheusClient({ + pushgateway: { enabled: true, jobName: "ghost-test" }, + }); instance.init(); await instance.pushMetrics(); scope.done(); }); - it('should log an error with error code if pushing metrics to the gateway fails', async function () { - instance = new PrometheusClient({pushgateway: {enabled: true, jobName: 'ghost-test'}}, logger); + it("should log an error with error code if pushing metrics to the gateway fails", async function () { + instance = new PrometheusClient( + { pushgateway: { enabled: true, jobName: "ghost-test" } }, + logger, + ); instance.init(); instance.gateway = { - pushAdd: sinon.stub().rejects({code: 'ECONNRESET'}) + pushAdd: sinon.stub().rejects({ code: "ECONNRESET" }), } as unknown as Pushgateway; await instance.pushMetrics(); assert.ok(logger.error.called); @@ -114,11 +121,14 @@ describe('Prometheus Client', function () { assert.match(error, /ECONNRESET/); }); - it('should log a generic error if the error is unknown', async function () { - instance = new PrometheusClient({pushgateway: {enabled: true, jobName: 'ghost-test'}}, logger); + it("should log a generic error if the error is unknown", async function () { + instance = new PrometheusClient( + { pushgateway: { enabled: true, jobName: "ghost-test" } }, + logger, + ); instance.init(); instance.gateway = { - pushAdd: sinon.stub().rejects() + pushAdd: sinon.stub().rejects(), } as unknown as Pushgateway; await instance.pushMetrics(); assert.ok(logger.error.called); @@ -126,12 +136,15 @@ describe('Prometheus Client', function () { assert.match(error, /Unknown error/); }); - it('should give up after 3 retries in a row', async function () { - instance = new PrometheusClient({pushgateway: {enabled: true, jobName: 'ghost-test'}}, logger); + it("should give up after 3 retries in a row", async function () { + instance = new PrometheusClient( + { pushgateway: { enabled: true, jobName: "ghost-test" } }, + logger, + ); instance.init(); const pushAddStub = sinon.stub().rejects(); instance.gateway = { - pushAdd: pushAddStub + pushAdd: pushAddStub, } as unknown as Pushgateway; // Simulate failing to push metrics multiple times in a row @@ -144,50 +157,54 @@ describe('Prometheus Client', function () { await instance.pushMetrics(); await instance.pushMetrics(); assert.ok(pushAddStub.calledThrice); - assert.ok(logger.error.calledWith('Failed to push metrics to pushgateway 3 times in a row, giving up')); + assert.ok( + logger.error.calledWith( + "Failed to push metrics to pushgateway 3 times in a row, giving up", + ), + ); }); }); - describe('handleMetricsRequest', function () { - it('should return the metrics', async function () { + describe("handleMetricsRequest", function () { + it("should return the metrics", async function () { const setStub = sinon.stub(); const endStub = sinon.stub(); const req = {} as Request; const res = { set: setStub, - end: endStub + end: endStub, } as unknown as Response; await instance.handleMetricsRequest(req, res); - assert.ok(setStub.calledWith('Content-Type', instance.getContentType())); + assert.ok(setStub.calledWith("Content-Type", instance.getContentType())); assert.ok(endStub.calledOnce); }); - it('should return an error if getting metrics fails', async function () { + it("should return an error if getting metrics fails", async function () { instance = new PrometheusClient(); - sinon.stub(instance, 'getMetrics').throws(new Error('Failed to get metrics')); + sinon.stub(instance, "getMetrics").throws(new Error("Failed to get metrics")); const statusStub = sinon.stub().returnsThis(); const endStub = sinon.stub(); const req = {} as Request; const res = { set: sinon.stub(), end: endStub, - status: statusStub + status: statusStub, } as unknown as Response; await instance.handleMetricsRequest(req, res); assert.ok(statusStub.calledWith(500)); assert.ok(endStub.calledOnce); }); - it('should return a generic error if the error is unknown', async function () { + it("should return a generic error if the error is unknown", async function () { instance = new PrometheusClient(); - sinon.stub(instance, 'getMetrics').throws({name: 'UnknownError'}); + sinon.stub(instance, "getMetrics").throws({ name: "UnknownError" }); const statusStub = sinon.stub().returnsThis(); const endStub = sinon.stub(); const req = {} as Request; const res = { set: sinon.stub(), end: endStub, - status: statusStub + status: statusStub, } as unknown as Response; await instance.handleMetricsRequest(req, res); assert.ok(statusStub.calledWith(500)); @@ -195,29 +212,29 @@ describe('Prometheus Client', function () { }); }); - describe('getMetrics', function () { - it('should return metrics as a string', async function () { + describe("getMetrics", function () { + it("should return metrics as a string", async function () { instance = new PrometheusClient(); instance.init(); const metrics = await instance.getMetrics(); - assert.equal(typeof metrics, 'string'); + assert.equal(typeof metrics, "string"); assert.match(metrics as string, /^# HELP/); }); }); - describe('getMetricsAsJSON', function () { - it('should return metrics as an array of objects', async function () { + describe("getMetricsAsJSON", function () { + it("should return metrics as an array of objects", async function () { instance = new PrometheusClient(); instance.init(); const metrics = await instance.getMetricsAsJSON(); - assert.equal(typeof metrics, 'object'); + assert.equal(typeof metrics, "object"); assert.ok(Array.isArray(metrics)); - assert.ok(Object.keys(metrics[0]).includes('name')); + assert.ok(Object.keys(metrics[0]).includes("name")); }); }); - describe('getMetricsAsArray', function () { - it('should return metrics as an array', async function () { + describe("getMetricsAsArray", function () { + it("should return metrics as an array", async function () { instance = new PrometheusClient(); instance.init(); const metricsArray = await instance.getMetricsAsArray(); @@ -226,77 +243,80 @@ describe('Prometheus Client', function () { }); }); - describe('getMetric', function () { - it('should return a metric from the registry by name', async function () { + describe("getMetric", function () { + it("should return a metric from the registry by name", async function () { instance = new PrometheusClient(); instance.init(); - const metric = instance.getMetric('ghost_process_cpu_seconds_total'); + const metric = instance.getMetric("ghost_process_cpu_seconds_total"); assert.ok(metric); }); - it('should return undefined if the metric is not found', function () { + it("should return undefined if the metric is not found", function () { instance = new PrometheusClient(); instance.init(); - const metric = instance.getMetric('ghost_not_a_metric'); + const metric = instance.getMetric("ghost_not_a_metric"); assert.equal(metric, undefined); }); - it('should add the prefix to the metric name if it is not already present', function () { + it("should add the prefix to the metric name if it is not already present", function () { instance = new PrometheusClient(); instance.init(); - const metric = instance.getMetric('process_cpu_seconds_total'); + const metric = instance.getMetric("process_cpu_seconds_total"); assert.ok(metric); }); }); - describe('getMetricObject', function () { - it('should return the values of a metric', async function () { + describe("getMetricObject", function () { + it("should return the values of a metric", async function () { instance = new PrometheusClient(); instance.init(); - const metricObject = await instance.getMetricObject('ghost_process_cpu_seconds_total'); + const metricObject = await instance.getMetricObject("ghost_process_cpu_seconds_total"); assert.ok(metricObject); assert.ok(metricObject.values); assert.ok(Array.isArray(metricObject.values)); - assert.equal(metricObject.help, 'Total user and system CPU time spent in seconds.'); - assert.equal(metricObject.type, 'counter'); - assert.equal(metricObject.name, 'ghost_process_cpu_seconds_total'); + assert.equal(metricObject.help, "Total user and system CPU time spent in seconds."); + assert.equal(metricObject.type, "counter"); + assert.equal(metricObject.name, "ghost_process_cpu_seconds_total"); }); - it('should return undefined if the metric is not found', async function () { + it("should return undefined if the metric is not found", async function () { instance = new PrometheusClient(); instance.init(); - const metricObject = await instance.getMetricObject('ghost_not_a_metric'); + const metricObject = await instance.getMetricObject("ghost_not_a_metric"); assert.equal(metricObject, undefined); }); }); - describe('getMetricValues', function () { - it('should return the values of a metric', async function () { + describe("getMetricValues", function () { + it("should return the values of a metric", async function () { instance = new PrometheusClient(); instance.init(); - const metricValues = await instance.getMetricValues('ghost_process_cpu_seconds_total'); + const metricValues = await instance.getMetricValues("ghost_process_cpu_seconds_total"); assert.ok(metricValues); assert.ok(Array.isArray(metricValues)); }); - it('should return undefined if the metric is not found', async function () { + it("should return undefined if the metric is not found", async function () { instance = new PrometheusClient(); instance.init(); - const metricValues = await instance.getMetricValues('ghost_not_a_metric'); + const metricValues = await instance.getMetricValues("ghost_not_a_metric"); assert.equal(metricValues, undefined); }); }); - describe('instrumentKnex', function () { + describe("instrumentKnex", function () { let knexMock: Knex; let knexEventEmitter: EventEmitterType; let poolEventEmitter: EventEmitterType; function simulateQuery(queryUid: string, duration: number) { const clock = sinon.useFakeTimers(); - knexEventEmitter.emit('query', {__knexQueryUid: queryUid, sql: 'SELECT 1'}); + knexEventEmitter.emit("query", { __knexQueryUid: queryUid, sql: "SELECT 1" }); clock.tick(duration); - knexEventEmitter.emit('query-response', null, {__knexQueryUid: queryUid, sql: 'SELECT 1'}); + knexEventEmitter.emit("query-response", null, { + __knexQueryUid: queryUid, + sql: "SELECT 1", + }); clock.restore(); } @@ -308,33 +328,33 @@ describe('Prometheus Client', function () { function simulateAcquire(duration: number) { const clock = sinon.useFakeTimers(); - poolEventEmitter.emit('acquireRequest'); + poolEventEmitter.emit("acquireRequest"); clock.tick(duration); - poolEventEmitter.emit('acquireSuccess'); + poolEventEmitter.emit("acquireSuccess"); clock.restore(); } function simulateAcquireFail(duration: number) { const clock = sinon.useFakeTimers(); - poolEventEmitter.emit('acquireRequest'); + poolEventEmitter.emit("acquireRequest"); clock.tick(duration); - poolEventEmitter.emit('acquireFail'); + poolEventEmitter.emit("acquireFail"); clock.restore(); } function simulateCreate(duration: number) { const clock = sinon.useFakeTimers(); - poolEventEmitter.emit('createRequest'); + poolEventEmitter.emit("createRequest"); clock.tick(duration); - poolEventEmitter.emit('createSuccess'); + poolEventEmitter.emit("createSuccess"); clock.restore(); } function simulateCreateFail(duration: number) { const clock = sinon.useFakeTimers(); - poolEventEmitter.emit('createRequest'); + poolEventEmitter.emit("createRequest"); clock.tick(duration); - poolEventEmitter.emit('createFail'); + poolEventEmitter.emit("createFail"); clock.restore(); } @@ -355,9 +375,9 @@ describe('Prometheus Client', function () { numPendingCreates: sinon.stub().returns(0), on: sinon.stub().callsFake((event, callback) => { poolEventEmitter.on(event, callback); - }) - } - } + }), + }, + }, } as unknown as Knex; }); @@ -365,449 +385,524 @@ describe('Prometheus Client', function () { sinon.restore(); }); - it('should collect the connection pool max metric', async function () { + it("should collect the connection pool max metric", async function () { instance = new PrometheusClient(); instance.init(); instance.instrumentKnex(knexMock); - const metricValues = await instance.getMetricValues('db_connection_pool_max'); + const metricValues = await instance.getMetricValues("db_connection_pool_max"); assert.equal(metricValues?.[0].value, 10); }); - it('should collect the connection pool min metric', async function () { + it("should collect the connection pool min metric", async function () { instance = new PrometheusClient(); instance.init(); instance.instrumentKnex(knexMock); - const metricValues = await instance.getMetricValues('db_connection_pool_min'); + const metricValues = await instance.getMetricValues("db_connection_pool_min"); assert.equal(metricValues?.[0].value, 1); }); - it('should collect the connection pool active metric', async function () { + it("should collect the connection pool active metric", async function () { knexMock.client.pool.numUsed = sinon.stub().returns(3); knexMock.client.pool.numFree = sinon.stub().returns(7); instance = new PrometheusClient(); instance.init(); instance.instrumentKnex(knexMock); - const metricValues = await instance.getMetricValues('db_connection_pool_active'); + const metricValues = await instance.getMetricValues("db_connection_pool_active"); assert.equal(metricValues?.[0].value, 10); }); - it('should collect the connection pool used metric', async function () { + it("should collect the connection pool used metric", async function () { knexMock.client.pool.numUsed = sinon.stub().returns(3); instance = new PrometheusClient(); instance.init(); instance.instrumentKnex(knexMock); - const metricValues = await instance.getMetricValues('db_connection_pool_used'); + const metricValues = await instance.getMetricValues("db_connection_pool_used"); assert.equal(metricValues?.[0].value, 3); }); - it('should collect the connection pool idle metric', async function () { + it("should collect the connection pool idle metric", async function () { knexMock.client.pool.numFree = sinon.stub().returns(7); instance = new PrometheusClient(); instance.init(); instance.instrumentKnex(knexMock); - const metricValues = await instance.getMetricValues('db_connection_pool_idle'); + const metricValues = await instance.getMetricValues("db_connection_pool_idle"); assert.equal(metricValues?.[0].value, 7); }); - it('should collect the connection pool pending acquires metric', async function () { + it("should collect the connection pool pending acquires metric", async function () { knexMock.client.pool.numPendingAcquires = sinon.stub().returns(3); instance = new PrometheusClient(); instance.init(); instance.instrumentKnex(knexMock); - const metricValues = await instance.getMetricValues('db_connection_pool_pending_acquires'); + const metricValues = await instance.getMetricValues( + "db_connection_pool_pending_acquires", + ); assert.equal(metricValues?.[0].value, 3); }); - it('should collect the connection pool pending creates metric', async function () { + it("should collect the connection pool pending creates metric", async function () { knexMock.client.pool.numPendingCreates = sinon.stub().returns(3); instance = new PrometheusClient(); instance.init(); instance.instrumentKnex(knexMock); - const metricValues = await instance.getMetricValues('db_connection_pool_pending_creates'); + const metricValues = await instance.getMetricValues( + "db_connection_pool_pending_creates", + ); assert.equal(metricValues?.[0].value, 3); }); - it('should collect the db query duration metric when a query is executed', async function () { + it("should collect the db query duration metric when a query is executed", async function () { instance = new PrometheusClient(); instance.init(); instance.instrumentKnex(knexMock); - simulateQuery('1', 500); - const metricValues = await instance.getMetricValues('db_query_duration_seconds'); + simulateQuery("1", 500); + const metricValues = await instance.getMetricValues("db_query_duration_seconds"); assert.equal(metricValues?.[0].value, 0.5); }); - it('should accurately calculate the query duration of a query', async function () { + it("should accurately calculate the query duration of a query", async function () { instance = new PrometheusClient(); instance.init(); instance.instrumentKnex(knexMock); const durations = [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]; simulateQueries(durations); - const metricValues = await instance.getMetricValues('db_query_duration_seconds'); + const metricValues = await instance.getMetricValues("db_query_duration_seconds"); assert.deepEqual(metricValues, [ - {labels: {quantile: 0.5}, value: 0.55}, - {labels: {quantile: 0.9}, value: 0.95}, - {labels: {quantile: 0.99}, value: 1}, - {metricName: 'ghost_db_query_duration_seconds_sum', labels: {}, value: 5.5}, - {metricName: 'ghost_db_query_duration_seconds_count', labels: {}, value: 10} + { labels: { quantile: 0.5 }, value: 0.55 }, + { labels: { quantile: 0.9 }, value: 0.95 }, + { labels: { quantile: 0.99 }, value: 1 }, + { metricName: "ghost_db_query_duration_seconds_sum", labels: {}, value: 5.5 }, + { metricName: "ghost_db_query_duration_seconds_count", labels: {}, value: 10 }, ]); }); - it('should collect the db connection acquire duration metric when a connection is acquired', async function () { + it("should collect the db connection acquire duration metric when a connection is acquired", async function () { instance = new PrometheusClient(); instance.init(); instance.instrumentKnex(knexMock); simulateAcquire(500); - const metricValues = await instance.getMetricValues('db_connection_acquire_duration_seconds'); + const metricValues = await instance.getMetricValues( + "db_connection_acquire_duration_seconds", + ); assert.equal(metricValues?.[0].value, 0.5); }); - it('should collect the db connection acquire duration metric when a connection fails to be acquired', async function () { + it("should collect the db connection acquire duration metric when a connection fails to be acquired", async function () { instance = new PrometheusClient(); instance.init(); instance.instrumentKnex(knexMock); simulateAcquireFail(500); - const metricValues = await instance.getMetricValues('db_connection_acquire_duration_seconds'); + const metricValues = await instance.getMetricValues( + "db_connection_acquire_duration_seconds", + ); assert.equal(metricValues?.[0].value, 0.5); }); - it('should collect the db connection create duration metrics when a connection is created', async function () { + it("should collect the db connection create duration metrics when a connection is created", async function () { instance = new PrometheusClient(); instance.init(); instance.instrumentKnex(knexMock); simulateCreate(500); - const metricValues = await instance.getMetricValues('db_connection_create_duration_seconds'); + const metricValues = await instance.getMetricValues( + "db_connection_create_duration_seconds", + ); assert.equal(metricValues?.[0].value, 0.5); }); - it('should collect the db connection create duration metrics when a connection fails to be created', async function () { + it("should collect the db connection create duration metrics when a connection fails to be created", async function () { instance = new PrometheusClient(); instance.init(); instance.instrumentKnex(knexMock); simulateCreateFail(500); - const metricValues = await instance.getMetricValues('db_connection_create_duration_seconds'); + const metricValues = await instance.getMetricValues( + "db_connection_create_duration_seconds", + ); assert.equal(metricValues?.[0].value, 0.5); }); }); - describe('Custom Metrics', function () { - describe('registerCounter', function () { - it('should add the counter metric to the registry', function () { + describe("Custom Metrics", function () { + describe("registerCounter", function () { + it("should add the counter metric to the registry", function () { instance = new PrometheusClient(); instance.init(); - instance.registerCounter({name: 'test_counter', help: 'A test counter'}); - const metric = instance.getMetric('ghost_test_counter'); + instance.registerCounter({ name: "test_counter", help: "A test counter" }); + const metric = instance.getMetric("ghost_test_counter"); assert.ok(metric); }); - it('should return the counter metric', function () { + it("should return the counter metric", function () { instance = new PrometheusClient(); instance.init(); - const counter = instance.registerCounter({name: 'test_counter', help: 'A test counter'}); - const metric = instance.getMetric('ghost_test_counter'); + const counter = instance.registerCounter({ + name: "test_counter", + help: "A test counter", + }); + const metric = instance.getMetric("ghost_test_counter"); assert.equal(metric, counter); }); - it('should increment the counter', async function () { + it("should increment the counter", async function () { instance = new PrometheusClient(); instance.init(); - const counter = instance.registerCounter({name: 'test_counter', help: 'A test counter'}); - const metricValuesBefore = await instance.getMetricValues('ghost_test_counter'); - assert.deepEqual(metricValuesBefore, [{value: 0, labels: {}}]); + const counter = instance.registerCounter({ + name: "test_counter", + help: "A test counter", + }); + const metricValuesBefore = await instance.getMetricValues("ghost_test_counter"); + assert.deepEqual(metricValuesBefore, [{ value: 0, labels: {} }]); counter.inc(); - const metricValuesAfter = await instance.getMetricValues('ghost_test_counter'); - assert.deepEqual(metricValuesAfter, [{value: 1, labels: {}}]); + const metricValuesAfter = await instance.getMetricValues("ghost_test_counter"); + assert.deepEqual(metricValuesAfter, [{ value: 1, labels: {} }]); }); }); - describe('registerGauge', function () { - it('should add the gauge metric to the registry', function () { + describe("registerGauge", function () { + it("should add the gauge metric to the registry", function () { instance = new PrometheusClient(); instance.init(); - instance.registerGauge({name: 'test_gauge', help: 'A test gauge'}); - const metric = instance.getMetric('ghost_test_gauge'); + instance.registerGauge({ name: "test_gauge", help: "A test gauge" }); + const metric = instance.getMetric("ghost_test_gauge"); assert.ok(metric); }); - it('should return the gauge metric', function () { + it("should return the gauge metric", function () { instance = new PrometheusClient(); instance.init(); - const gauge = instance.registerGauge({name: 'test_gauge', help: 'A test gauge'}); - const metric = instance.getMetric('ghost_test_gauge'); + const gauge = instance.registerGauge({ name: "test_gauge", help: "A test gauge" }); + const metric = instance.getMetric("ghost_test_gauge"); assert.equal(metric, gauge); }); - it('should set the gauge value', async function () { + it("should set the gauge value", async function () { instance = new PrometheusClient(); instance.init(); - const gauge = instance.registerGauge({name: 'test_gauge', help: 'A test gauge'}); + const gauge = instance.registerGauge({ name: "test_gauge", help: "A test gauge" }); gauge.set(10); - const metricValues = await instance.getMetricValues('ghost_test_gauge'); - assert.deepEqual(metricValues, [{value: 10, labels: {}}]); + const metricValues = await instance.getMetricValues("ghost_test_gauge"); + assert.deepEqual(metricValues, [{ value: 10, labels: {} }]); }); - it('should increment the gauge', async function () { + it("should increment the gauge", async function () { instance = new PrometheusClient(); instance.init(); - const gauge = instance.registerGauge({name: 'test_gauge', help: 'A test gauge'}); - const metricValuesBefore = await instance.getMetricValues('ghost_test_gauge'); - assert.deepEqual(metricValuesBefore, [{value: 0, labels: {}}]); + const gauge = instance.registerGauge({ name: "test_gauge", help: "A test gauge" }); + const metricValuesBefore = await instance.getMetricValues("ghost_test_gauge"); + assert.deepEqual(metricValuesBefore, [{ value: 0, labels: {} }]); gauge.inc(); - const metricValuesAfter = await instance.getMetricValues('ghost_test_gauge'); - assert.deepEqual(metricValuesAfter, [{value: 1, labels: {}}]); + const metricValuesAfter = await instance.getMetricValues("ghost_test_gauge"); + assert.deepEqual(metricValuesAfter, [{ value: 1, labels: {} }]); }); - it('should decrement the gauge', async function () { + it("should decrement the gauge", async function () { instance = new PrometheusClient(); instance.init(); - const gauge = instance.registerGauge({name: 'test_gauge', help: 'A test gauge'}); - const metricValuesBefore = await instance.getMetricValues('ghost_test_gauge'); - assert.deepEqual(metricValuesBefore, [{value: 0, labels: {}}]); + const gauge = instance.registerGauge({ name: "test_gauge", help: "A test gauge" }); + const metricValuesBefore = await instance.getMetricValues("ghost_test_gauge"); + assert.deepEqual(metricValuesBefore, [{ value: 0, labels: {} }]); gauge.dec(); - const metricValuesAfter = await instance.getMetricValues('ghost_test_gauge'); - assert.deepEqual(metricValuesAfter, [{value: -1, labels: {}}]); + const metricValuesAfter = await instance.getMetricValues("ghost_test_gauge"); + assert.deepEqual(metricValuesAfter, [{ value: -1, labels: {} }]); }); - it('should use the collect function to set the gauge value', async function () { + it("should use the collect function to set the gauge value", async function () { instance = new PrometheusClient(); instance.init(); - instance.registerGauge({name: 'test_gauge', help: 'A test gauge', collect() { - (this as unknown as Gauge).set(10); // `this` is the gauge instance - }}); - const metricValues = await instance.getMetricValues('ghost_test_gauge'); - assert.deepEqual(metricValues, [{value: 10, labels: {}}]); + instance.registerGauge({ + name: "test_gauge", + help: "A test gauge", + collect() { + (this as unknown as Gauge).set(10); // `this` is the gauge instance + }, + }); + const metricValues = await instance.getMetricValues("ghost_test_gauge"); + assert.deepEqual(metricValues, [{ value: 10, labels: {} }]); }); - it('should use an async collect function to set the gauge value', async function () { + it("should use an async collect function to set the gauge value", async function () { instance = new PrometheusClient(); instance.init(); - instance.registerGauge({name: 'test_gauge', help: 'A test gauge', async collect() { - await new Promise((resolve) => { - setTimeout(resolve, 10); - }); - (this as unknown as Gauge).set(20); // `this` is the gauge instance - }}); - const metricValues = await instance.getMetricValues('ghost_test_gauge'); - assert.deepEqual(metricValues, [{value: 20, labels: {}}]); + instance.registerGauge({ + name: "test_gauge", + help: "A test gauge", + async collect() { + await new Promise((resolve) => { + setTimeout(resolve, 10); + }); + (this as unknown as Gauge).set(20); // `this` is the gauge instance + }, + }); + const metricValues = await instance.getMetricValues("ghost_test_gauge"); + assert.deepEqual(metricValues, [{ value: 20, labels: {} }]); }); }); - describe('registerSummary', function () { - it('should add the summary metric to the registry', function () { + describe("registerSummary", function () { + it("should add the summary metric to the registry", function () { instance = new PrometheusClient(); instance.init(); - instance.registerSummary({name: 'test_summary', help: 'A test summary'}); - const metric = instance.getMetric('ghost_test_summary'); + instance.registerSummary({ name: "test_summary", help: "A test summary" }); + const metric = instance.getMetric("ghost_test_summary"); assert.ok(metric); }); - it('should return the summary metric', function () { + it("should return the summary metric", function () { instance = new PrometheusClient(); instance.init(); - const summary = instance.registerSummary({name: 'test_summary', help: 'A test summary'}); - const metric = instance.getMetric('ghost_test_summary'); + const summary = instance.registerSummary({ + name: "test_summary", + help: "A test summary", + }); + const metric = instance.getMetric("ghost_test_summary"); assert.equal(metric, summary); }); - it('can observe a value', async function () { + it("can observe a value", async function () { instance = new PrometheusClient(); instance.init(); - const summary = instance.registerSummary({name: 'test_summary', help: 'A test summary'}); + const summary = instance.registerSummary({ + name: "test_summary", + help: "A test summary", + }); summary.observe(10); - const metricValues = await instance.getMetricValues('ghost_test_summary'); + const metricValues = await instance.getMetricValues("ghost_test_summary"); assert.deepEqual(metricValues, [ - {labels: {quantile: 0.5}, value: 10}, - {labels: {quantile: 0.9}, value: 10}, - {labels: {quantile: 0.99}, value: 10}, - {metricName: 'ghost_test_summary_sum', labels: {}, value: 10}, - {metricName: 'ghost_test_summary_count', labels: {}, value: 1} + { labels: { quantile: 0.5 }, value: 10 }, + { labels: { quantile: 0.9 }, value: 10 }, + { labels: { quantile: 0.99 }, value: 10 }, + { metricName: "ghost_test_summary_sum", labels: {}, value: 10 }, + { metricName: "ghost_test_summary_count", labels: {}, value: 1 }, ]); }); - it('can use the collect function to set the summary value', async function () { + it("can use the collect function to set the summary value", async function () { instance = new PrometheusClient(); instance.init(); - instance.registerSummary({name: 'test_summary', help: 'A test summary', collect() { - (this as unknown as Summary).observe(20); - }}); - const metricValues = await instance.getMetricValues('ghost_test_summary'); + instance.registerSummary({ + name: "test_summary", + help: "A test summary", + collect() { + (this as unknown as Summary).observe(20); + }, + }); + const metricValues = await instance.getMetricValues("ghost_test_summary"); assert.deepEqual(metricValues, [ - {labels: {quantile: 0.5}, value: 20}, - {labels: {quantile: 0.9}, value: 20}, - {labels: {quantile: 0.99}, value: 20}, - {metricName: 'ghost_test_summary_sum', labels: {}, value: 20}, - {metricName: 'ghost_test_summary_count', labels: {}, value: 1} + { labels: { quantile: 0.5 }, value: 20 }, + { labels: { quantile: 0.9 }, value: 20 }, + { labels: { quantile: 0.99 }, value: 20 }, + { metricName: "ghost_test_summary_sum", labels: {}, value: 20 }, + { metricName: "ghost_test_summary_count", labels: {}, value: 1 }, ]); }); - it('can use an async collect function to set the summary value', async function () { + it("can use an async collect function to set the summary value", async function () { instance = new PrometheusClient(); instance.init(); - instance.registerSummary({name: 'test_summary', help: 'A test summary', async collect() { - await new Promise((resolve) => { - setTimeout(resolve, 10); - }); - (this as unknown as Summary).observe(30); - }}); - const metricValues = await instance.getMetricValues('ghost_test_summary'); + instance.registerSummary({ + name: "test_summary", + help: "A test summary", + async collect() { + await new Promise((resolve) => { + setTimeout(resolve, 10); + }); + (this as unknown as Summary).observe(30); + }, + }); + const metricValues = await instance.getMetricValues("ghost_test_summary"); assert.deepEqual(metricValues, [ - {labels: {quantile: 0.5}, value: 30}, - {labels: {quantile: 0.9}, value: 30}, - {labels: {quantile: 0.99}, value: 30}, - {metricName: 'ghost_test_summary_sum', labels: {}, value: 30}, - {metricName: 'ghost_test_summary_count', labels: {}, value: 1} + { labels: { quantile: 0.5 }, value: 30 }, + { labels: { quantile: 0.9 }, value: 30 }, + { labels: { quantile: 0.99 }, value: 30 }, + { metricName: "ghost_test_summary_sum", labels: {}, value: 30 }, + { metricName: "ghost_test_summary_count", labels: {}, value: 1 }, ]); }); - it('respects the percentiles option', async function () { + it("respects the percentiles option", async function () { instance = new PrometheusClient(); instance.init(); - instance.registerSummary({name: 'test_summary', help: 'A test summary', percentiles: [0.1, 0.5, 0.9]}); - const metricValues = await instance.getMetricValues('ghost_test_summary'); + instance.registerSummary({ + name: "test_summary", + help: "A test summary", + percentiles: [0.1, 0.5, 0.9], + }); + const metricValues = await instance.getMetricValues("ghost_test_summary"); assert.deepEqual(metricValues, [ - {labels: {quantile: 0.1}, value: 0}, - {labels: {quantile: 0.5}, value: 0}, - {labels: {quantile: 0.9}, value: 0}, - {metricName: 'ghost_test_summary_sum', labels: {}, value: 0}, - {metricName: 'ghost_test_summary_count', labels: {}, value: 0} + { labels: { quantile: 0.1 }, value: 0 }, + { labels: { quantile: 0.5 }, value: 0 }, + { labels: { quantile: 0.9 }, value: 0 }, + { metricName: "ghost_test_summary_sum", labels: {}, value: 0 }, + { metricName: "ghost_test_summary_count", labels: {}, value: 0 }, ]); }); - it('removes datapoints older than maxAgeSeconds from percentile metrics if maxAgeSeconds and ageBuckets are provided', async function () { + it("removes datapoints older than maxAgeSeconds from percentile metrics if maxAgeSeconds and ageBuckets are provided", async function () { const clock = sinon.useFakeTimers(); instance = new PrometheusClient(); instance.init(); - const metric = instance.registerSummary({name: 'test_summary', help: 'A test summary', maxAgeSeconds: 10, ageBuckets: 1}); + const metric = instance.registerSummary({ + name: "test_summary", + help: "A test summary", + maxAgeSeconds: 10, + ageBuckets: 1, + }); metric.observe(1); - const metricValuesBefore = await instance.getMetricValues('ghost_test_summary'); + const metricValuesBefore = await instance.getMetricValues("ghost_test_summary"); assert.deepEqual(metricValuesBefore, [ - {labels: {quantile: 0.5}, value: 1}, - {labels: {quantile: 0.9}, value: 1}, - {labels: {quantile: 0.99}, value: 1}, - {metricName: 'ghost_test_summary_sum', labels: {}, value: 1}, - {metricName: 'ghost_test_summary_count', labels: {}, value: 1} + { labels: { quantile: 0.5 }, value: 1 }, + { labels: { quantile: 0.9 }, value: 1 }, + { labels: { quantile: 0.99 }, value: 1 }, + { metricName: "ghost_test_summary_sum", labels: {}, value: 1 }, + { metricName: "ghost_test_summary_count", labels: {}, value: 1 }, ]); clock.tick(20000); - const metricValuesAfter = await instance.getMetricValues('ghost_test_summary'); + const metricValuesAfter = await instance.getMetricValues("ghost_test_summary"); assert.deepEqual(metricValuesAfter, [ - {labels: {quantile: 0.5}, value: 0}, - {labels: {quantile: 0.9}, value: 0}, - {labels: {quantile: 0.99}, value: 0}, - {metricName: 'ghost_test_summary_sum', labels: {}, value: 1}, - {metricName: 'ghost_test_summary_count', labels: {}, value: 1} + { labels: { quantile: 0.5 }, value: 0 }, + { labels: { quantile: 0.9 }, value: 0 }, + { labels: { quantile: 0.99 }, value: 0 }, + { metricName: "ghost_test_summary_sum", labels: {}, value: 1 }, + { metricName: "ghost_test_summary_count", labels: {}, value: 1 }, ]); clock.restore(); }); - it('does not export the metric if maxAgeSeconds and ageBuckets are provided and pruneAgedBuckets is true', async function () { + it("does not export the metric if maxAgeSeconds and ageBuckets are provided and pruneAgedBuckets is true", async function () { const clock = sinon.useFakeTimers(); instance = new PrometheusClient(); instance.init(); - const metric = instance.registerSummary({name: 'test_summary', help: 'A test summary', maxAgeSeconds: 10, ageBuckets: 1, pruneAgedBuckets: true}); + const metric = instance.registerSummary({ + name: "test_summary", + help: "A test summary", + maxAgeSeconds: 10, + ageBuckets: 1, + pruneAgedBuckets: true, + }); metric.observe(1); clock.tick(20000); - const metricValues = await instance.getMetricValues('ghost_test_summary'); + const metricValues = await instance.getMetricValues("ghost_test_summary"); assert.deepEqual(metricValues, []); clock.restore(); }); - it('can use a timer to observe the summary value', async function () { + it("can use a timer to observe the summary value", async function () { instance = new PrometheusClient(); instance.init(); - const summary = instance.registerSummary({name: 'test_summary', help: 'A test summary', percentiles: [0.1, 0.5, 0.9]}); + const summary = instance.registerSummary({ + name: "test_summary", + help: "A test summary", + percentiles: [0.1, 0.5, 0.9], + }); const clock = sinon.useFakeTimers(); const timer = summary.startTimer(); clock.tick(1000); timer(); - const metricValues = await instance.getMetricValues('ghost_test_summary'); + const metricValues = await instance.getMetricValues("ghost_test_summary"); assert.deepEqual(metricValues, [ - {labels: {quantile: 0.1}, value: 1}, - {labels: {quantile: 0.5}, value: 1}, - {labels: {quantile: 0.9}, value: 1}, - {metricName: 'ghost_test_summary_sum', labels: {}, value: 1}, - {metricName: 'ghost_test_summary_count', labels: {}, value: 1} + { labels: { quantile: 0.1 }, value: 1 }, + { labels: { quantile: 0.5 }, value: 1 }, + { labels: { quantile: 0.9 }, value: 1 }, + { metricName: "ghost_test_summary_sum", labels: {}, value: 1 }, + { metricName: "ghost_test_summary_count", labels: {}, value: 1 }, ]); clock.restore(); }); }); - describe('registerHistogram', function () { - it('should add the histogram metric to the registry', function () { + describe("registerHistogram", function () { + it("should add the histogram metric to the registry", function () { instance = new PrometheusClient(); instance.init(); - instance.registerHistogram({name: 'test_histogram', help: 'A test histogram', buckets: [1, 2, 3]}); - const metric = instance.getMetric('ghost_test_histogram'); + instance.registerHistogram({ + name: "test_histogram", + help: "A test histogram", + buckets: [1, 2, 3], + }); + const metric = instance.getMetric("ghost_test_histogram"); assert.ok(metric); }); - it('should return the histogram metric', function () { + it("should return the histogram metric", function () { instance = new PrometheusClient(); instance.init(); - const histogram = instance.registerHistogram({name: 'test_histogram', help: 'A test histogram', buckets: [1, 2, 3]}); - const metric = instance.getMetric('ghost_test_histogram'); + const histogram = instance.registerHistogram({ + name: "test_histogram", + help: "A test histogram", + buckets: [1, 2, 3], + }); + const metric = instance.getMetric("ghost_test_histogram"); assert.equal(metric, histogram); }); - it('can observe a value', async function () { + it("can observe a value", async function () { instance = new PrometheusClient(); instance.init(); - const histogram = instance.registerHistogram({name: 'test_histogram', help: 'A test histogram', buckets: [1, 2, 3]}); + const histogram = instance.registerHistogram({ + name: "test_histogram", + help: "A test histogram", + buckets: [1, 2, 3], + }); histogram.observe(1); histogram.observe(2); histogram.observe(3); - const metricValues = await instance.getMetricValues('ghost_test_histogram'); + const metricValues = await instance.getMetricValues("ghost_test_histogram"); assert.deepEqual(metricValues, [ { exemplar: null, labels: { - le: 1 + le: 1, }, - metricName: 'ghost_test_histogram_bucket', - value: 1 + metricName: "ghost_test_histogram_bucket", + value: 1, }, { exemplar: null, labels: { - le: 2 + le: 2, }, - metricName: 'ghost_test_histogram_bucket', - value: 2 + metricName: "ghost_test_histogram_bucket", + value: 2, }, { exemplar: null, labels: { - le: 3 + le: 3, }, - metricName: 'ghost_test_histogram_bucket', - value: 3 + metricName: "ghost_test_histogram_bucket", + value: 3, }, { exemplar: null, labels: { - le: '+Inf' + le: "+Inf", }, - metricName: 'ghost_test_histogram_bucket', - value: 3 + metricName: "ghost_test_histogram_bucket", + value: 3, }, { exemplar: undefined, labels: {}, - metricName: 'ghost_test_histogram_sum', - value: 6 + metricName: "ghost_test_histogram_sum", + value: 6, }, { exemplar: undefined, labels: {}, - metricName: 'ghost_test_histogram_count', - value: 3 - } + metricName: "ghost_test_histogram_count", + value: 3, + }, ]); }); - it('can use a timer to observe the histogram value', async function () { + it("can use a timer to observe the histogram value", async function () { instance = new PrometheusClient(); instance.init(); - const histogram = instance.registerHistogram({name: 'test_histogram', help: 'A test histogram', buckets: [1000, 2000, 3000]}); + const histogram = instance.registerHistogram({ + name: "test_histogram", + help: "A test histogram", + buckets: [1000, 2000, 3000], + }); const clock = sinon.useFakeTimers(); // Observe a value of 1 second const timer1 = histogram.startTimer(); @@ -819,52 +914,52 @@ describe('Prometheus Client', function () { clock.tick(2000); timer2(); - const metricValues = await instance.getMetricValues('ghost_test_histogram'); + const metricValues = await instance.getMetricValues("ghost_test_histogram"); assert.deepEqual(metricValues, [ { exemplar: null, labels: { - le: 1000 + le: 1000, }, - metricName: 'ghost_test_histogram_bucket', - value: 2 + metricName: "ghost_test_histogram_bucket", + value: 2, }, { exemplar: null, labels: { - le: 2000 + le: 2000, }, - metricName: 'ghost_test_histogram_bucket', - value: 2 + metricName: "ghost_test_histogram_bucket", + value: 2, }, { exemplar: null, labels: { - le: 3000 + le: 3000, }, - metricName: 'ghost_test_histogram_bucket', - value: 2 + metricName: "ghost_test_histogram_bucket", + value: 2, }, { exemplar: null, labels: { - le: '+Inf' + le: "+Inf", }, - metricName: 'ghost_test_histogram_bucket', - value: 2 + metricName: "ghost_test_histogram_bucket", + value: 2, }, { exemplar: undefined, labels: {}, - metricName: 'ghost_test_histogram_sum', - value: 3 + metricName: "ghost_test_histogram_sum", + value: 3, }, { exemplar: undefined, labels: {}, - metricName: 'ghost_test_histogram_count', - value: 2 - } + metricName: "ghost_test_histogram_count", + value: 2, + }, ]); clock.restore(); diff --git a/packages/prometheus-metrics/tsconfig.json b/packages/prometheus-metrics/tsconfig.json index 34d88a32c..faf5cd02d 100644 --- a/packages/prometheus-metrics/tsconfig.json +++ b/packages/prometheus-metrics/tsconfig.json @@ -1,107 +1,105 @@ { - "ts-node": { - "files": true - }, - "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - /* Projects */ - "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - /* Language and Environment */ - "target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - // "lib": ["es2019"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - /* Modules */ - "module": "commonjs", /* Specify what module code is generated. */ - "rootDir": "src", /* Specify the root folder within your source files. */ - // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ - // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ - // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ - // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ - "resolveJsonModule": true, /* Enable importing .json files. */ - // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - /* Emit */ - "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - "outDir": "build", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ - /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - }, - "include": [ - "src/**/*" - ] -} \ No newline at end of file + "ts-node": { + "files": true + }, + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + /* Projects */ + "incremental": true /* Save .tsbuildinfo files to allow for incremental compilation of projects. */, + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Language and Environment */ + "target": "es2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": ["es2019"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + /* Modules */ + "module": "commonjs" /* Specify what module code is generated. */, + "rootDir": "src" /* Specify the root folder within your source files. */, + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + "resolveJsonModule": true /* Enable importing .json files. */, + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* Emit */ + "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "build" /* Specify an output folder for all emitted files. */, + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */, + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["src/**/*"] +} diff --git a/packages/prometheus-metrics/vitest.config.ts b/packages/prometheus-metrics/vitest.config.ts index 0a51d09c0..ddc255fc9 100644 --- a/packages/prometheus-metrics/vitest.config.ts +++ b/packages/prometheus-metrics/vitest.config.ts @@ -1,12 +1,15 @@ -import {defineConfig, mergeConfig} from 'vitest/config'; -import rootConfig from '../../vitest.config'; +import { defineConfig, mergeConfig } from "vitest/config"; +import rootConfig from "../../vitest.config"; // Override: TypeScript package with source in src/, not lib/. // Coverage must be scoped to src/ to measure the right files. -export default mergeConfig(rootConfig, defineConfig({ - test: { - coverage: { - include: ['src/**'] - } - } -})); +export default mergeConfig( + rootConfig, + defineConfig({ + test: { + coverage: { + include: ["src/**"], + }, + }, + }), +); diff --git a/packages/promise/README.md b/packages/promise/README.md index c07c08f8b..4644cfcaf 100644 --- a/packages/promise/README.md +++ b/packages/promise/README.md @@ -8,36 +8,30 @@ or `yarn add @tryghost/promise` - ## Purpose Small async utility module with `pipeline`, `sequence`, and `pool` helpers for Promise workflows. ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests - - - # Copyright & License Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/promise/index.js b/packages/promise/index.js index 861605ebf..fc44c9491 100644 --- a/packages/promise/index.js +++ b/packages/promise/index.js @@ -1,13 +1,13 @@ module.exports = { get pipeline() { - return require('./lib/pipeline'); + return require("./lib/pipeline"); }, get sequence() { - return require('./lib/sequence'); + return require("./lib/sequence"); }, get pool() { - return require('./lib/pool'); - } + return require("./lib/pool"); + }, }; diff --git a/packages/promise/lib/pool.js b/packages/promise/lib/pool.js index 0d472409e..e2cfec798 100644 --- a/packages/promise/lib/pool.js +++ b/packages/promise/lib/pool.js @@ -8,19 +8,19 @@ async function pool(tasks, maxConcurrent) { if (maxConcurrent < 1) { // eslint-disable-next-line ghost/ghost-custom/no-native-error - throw new Error('Must set at least 1 concurrent workers'); // eslint-disable-line no-restricted-syntax + throw new Error("Must set at least 1 concurrent workers"); // eslint-disable-line no-restricted-syntax } const taskIterator = tasks.entries(); const results = []; - const workers = Array(maxConcurrent).fill(taskIterator).map( - async (workerIterator) => { + const workers = Array(maxConcurrent) + .fill(taskIterator) + .map(async (workerIterator) => { for (let [index, task] of workerIterator) { results[index] = await task(); } - } - ); + }); await Promise.all(workers); return results; } diff --git a/packages/promise/package.json b/packages/promise/package.json index bc17d4222..93f8cb41b 100644 --- a/packages/promise/package.json +++ b/packages/promise/package.json @@ -1,14 +1,21 @@ { "name": "@tryghost/promise", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/promise" }, - "author": "Ghost Foundation", - "license": "MIT", + "files": [ + "index.js", + "lib" + ], "main": "index.js", + "publishConfig": { + "access": "public" + }, "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", @@ -16,13 +23,6 @@ "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, - "files": [ - "index.js", - "lib" - ], - "publishConfig": { - "access": "public" - }, "devDependencies": { "sinon": "21.0.3" } diff --git a/packages/promise/test/.eslintrc.js b/packages/promise/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/promise/test/.eslintrc.js +++ b/packages/promise/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/promise/test/pipeline.test.js b/packages/promise/test/pipeline.test.js index 5894f5071..4209a096e 100644 --- a/packages/promise/test/pipeline.test.js +++ b/packages/promise/test/pipeline.test.js @@ -1,8 +1,8 @@ -const assert = require('assert/strict'); -const sinon = require('sinon'); +const assert = require("assert/strict"); +const sinon = require("sinon"); // Stuff we are testing -const {pipeline} = require('../'); +const { pipeline } = require("../"); // These tests are based on the tests in https://github.com/cujojs/when/blob/3.7.4/test/pipeline-test.js function createTask(y) { @@ -11,30 +11,32 @@ function createTask(y) { }; } -describe('Pipeline', function () { +describe("Pipeline", function () { afterEach(function () { sinon.restore(); }); - it('should execute tasks in order', function () { - return pipeline([createTask('b'), createTask('c'), createTask('d')], 'a').then(function (result) { - assert.equal(result, 'abcd'); - }); + it("should execute tasks in order", function () { + return pipeline([createTask("b"), createTask("c"), createTask("d")], "a").then( + function (result) { + assert.equal(result, "abcd"); + }, + ); }); - it('should resolve to initial args when no tasks supplied', function () { - return pipeline([], 'a', 'b').then(function (result) { - assert.deepEqual(result, ['a', 'b']); + it("should resolve to initial args when no tasks supplied", function () { + return pipeline([], "a", "b").then(function (result) { + assert.deepEqual(result, ["a", "b"]); }); }); - it('should resolve to empty array when no tasks and no args supplied', function () { + it("should resolve to empty array when no tasks and no args supplied", function () { return pipeline([]).then(function (result) { assert.deepEqual(result, []); }); }); - it('should pass args to initial task', function () { + it("should pass args to initial task", function () { const expected = [1, 2, 3]; const tasks = [sinon.spy()]; @@ -44,23 +46,25 @@ describe('Pipeline', function () { }); }); - it('should allow initial args to be promises', function () { + it("should allow initial args to be promises", function () { const expected = [1, 2, 3]; const tasks = [sinon.spy()]; - return pipeline(tasks, Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)).then(function () { - assert.equal(tasks[0].calledOnce, true); - assert.deepEqual(tasks[0].firstCall.args, expected); - }); + return pipeline(tasks, Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)).then( + function () { + assert.equal(tasks[0].calledOnce, true); + assert.deepEqual(tasks[0].firstCall.args, expected); + }, + ); }); - it('should allow tasks to be promises', function () { + it("should allow tasks to be promises", function () { const expected = [1, 2, 3]; const tasks = [ sinon.stub().returns(Promise.resolve(4)), sinon.stub().returns(Promise.resolve(5)), - sinon.stub().returns(Promise.resolve(6)) + sinon.stub().returns(Promise.resolve(6)), ]; return pipeline(tasks, 1, 2, 3).then(function (result) { @@ -74,23 +78,25 @@ describe('Pipeline', function () { }); }); - it('should allow tasks and args to be promises', function () { + it("should allow tasks and args to be promises", function () { const expected = [1, 2, 3]; const tasks = [ sinon.stub().returns(Promise.resolve(4)), sinon.stub().returns(Promise.resolve(5)), - sinon.stub().returns(Promise.resolve(6)) + sinon.stub().returns(Promise.resolve(6)), ]; - return pipeline(tasks, Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)).then(function (result) { - assert.equal(result, 6); - assert.equal(tasks[0].calledOnce, true); - assert.deepEqual(tasks[0].firstCall.args, expected); - assert.equal(tasks[1].calledOnce, true); - assert.equal(tasks[1].firstCall.calledWith(4), true); - assert.equal(tasks[2].calledOnce, true); - assert.equal(tasks[2].firstCall.calledWith(5), true); - }); + return pipeline(tasks, Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)).then( + function (result) { + assert.equal(result, 6); + assert.equal(tasks[0].calledOnce, true); + assert.deepEqual(tasks[0].firstCall.args, expected); + assert.equal(tasks[1].calledOnce, true); + assert.equal(tasks[1].firstCall.calledWith(4), true); + assert.equal(tasks[2].calledOnce, true); + assert.equal(tasks[2].firstCall.calledWith(5), true); + }, + ); }); }); diff --git a/packages/promise/test/pool.test.js b/packages/promise/test/pool.test.js index e69074ca3..7f4dc8ff3 100644 --- a/packages/promise/test/pool.test.js +++ b/packages/promise/test/pool.test.js @@ -1,65 +1,62 @@ -const assert = require('assert/strict'); -const {promisify} = require('util'); -const {pool} = require('../'); +const assert = require("assert/strict"); +const { promisify } = require("util"); +const { pool } = require("../"); -describe('Promise pool', function () { - it('preserves order', function () { +describe("Promise pool", function () { + it("preserves order", function () { const tasks = [ async function a() { await promisify(setTimeout)(100); - return Promise.resolve('hello'); + return Promise.resolve("hello"); }, function b() { - return Promise.resolve('hi'); - } + return Promise.resolve("hi"); + }, ]; - return pool(tasks, 3) - .then(function (result) { - assert.deepEqual(result, ['hello', 'hi']); - }); + return pool(tasks, 3).then(function (result) { + assert.deepEqual(result, ["hello", "hi"]); + }); }); - it('handles mixed promises and values', function () { + it("handles mixed promises and values", function () { const tasks = [ async function a() { - return 'hello'; + return "hello"; }, function b() { - return Promise.resolve('hi'); - } + return Promise.resolve("hi"); + }, ]; - return pool(tasks, 3) - .then(function (result) { - assert.deepEqual(result, ['hello', 'hi']); - }); + return pool(tasks, 3).then(function (result) { + assert.deepEqual(result, ["hello", "hi"]); + }); }); - it('does not allow less than 1 worker', async function () { + it("does not allow less than 1 worker", async function () { const tasks = [ async function a() { - return 'hello'; - } + return "hello"; + }, ]; await assert.rejects(pool(tasks, 0), { - name: 'Error', - message: 'Must set at least 1 concurrent workers' + name: "Error", + message: "Must set at least 1 concurrent workers", }); }); - it('does not affect results to have more workers than tasks', function () { + it("does not affect results to have more workers than tasks", function () { const tasks = [ async function a() { - return Promise.resolve('hi'); + return Promise.resolve("hi"); }, function b() { - return Promise.resolve('hello'); - } + return Promise.resolve("hello"); + }, ]; - return pool(tasks, 100) - .then(function (result) { - assert.deepEqual(result, ['hi', 'hello']); - }); + return pool(tasks, 100).then(function (result) { + assert.deepEqual(result, ["hi", "hello"]); + }); }); }); diff --git a/packages/promise/test/sequence.test.js b/packages/promise/test/sequence.test.js index 3f5b1fae7..dca2f97f7 100644 --- a/packages/promise/test/sequence.test.js +++ b/packages/promise/test/sequence.test.js @@ -1,27 +1,26 @@ -const assert = require('assert/strict'); -const sinon = require('sinon'); -const {sequence} = require('../'); +const assert = require("assert/strict"); +const sinon = require("sinon"); +const { sequence } = require("../"); -describe('Unit: lib/promise/sequence', function () { +describe("Unit: lib/promise/sequence", function () { afterEach(function () { sinon.restore(); }); - it('mixed tasks: promise and none promise', function () { + it("mixed tasks: promise and none promise", function () { const tasks = [ function a() { - return Promise.resolve('hello'); + return Promise.resolve("hello"); }, function b() { - return 'from'; + return "from"; }, function c() { - return Promise.resolve('chio'); - } + return Promise.resolve("chio"); + }, ]; - return sequence(tasks) - .then(function (result) { - assert.deepEqual(result, ['hello', 'from', 'chio']); - }); + return sequence(tasks).then(function (result) { + assert.deepEqual(result, ["hello", "from", "chio"]); + }); }); }); diff --git a/packages/request/README.md b/packages/request/README.md index 654532514..d838f09b3 100644 --- a/packages/request/README.md +++ b/packages/request/README.md @@ -8,36 +8,30 @@ or `yarn add @tryghost/request` - ## Purpose HTTP request abstraction used by Ghost for outbound calls, error handling, and response normalization. ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests +# Copyright & License - - -# Copyright & License - -Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/request/index.js b/packages/request/index.js index d29c393d2..6dfb87ddc 100644 --- a/packages/request/index.js +++ b/packages/request/index.js @@ -1 +1 @@ -module.exports = require('./lib/request'); +module.exports = require("./lib/request"); diff --git a/packages/request/lib/request.js b/packages/request/lib/request.js index 2e7bf534d..a5230c0e5 100644 --- a/packages/request/lib/request.js +++ b/packages/request/lib/request.js @@ -1,19 +1,19 @@ -const _ = require('lodash'); -const validator = require('@tryghost/validator'); -const errors = require('@tryghost/errors'); -const ghostVersion = require('@tryghost/version'); +const _ = require("lodash"); +const validator = require("@tryghost/validator"); +const errors = require("@tryghost/errors"); +const ghostVersion = require("@tryghost/version"); -const gotPromise = import('got'); -const cacheableLookupPromise = import('cacheable-lookup'); +const gotPromise = import("got"); +const cacheableLookupPromise = import("cacheable-lookup"); let got; let cacheableLookup; const defaultOptions = { headers: { - 'user-agent': 'Ghost/' + ghostVersion.safe + ' (https://github.com/TryGhost/Ghost)' + "user-agent": "Ghost/" + ghostVersion.safe + " (https://github.com/TryGhost/Ghost)", }, - method: 'GET' + method: "GET", }; module.exports = async function request(url, options = {}) { @@ -25,27 +25,32 @@ module.exports = async function request(url, options = {}) { // Ensure OS-level name resolution is not used const CacheableLookup = (await cacheableLookupPromise).default; cacheableLookup = new CacheableLookup({ - lookup: false + lookup: false, }); defaultOptions.dnsLookup = cacheableLookup.lookup; } if (_.isEmpty(url) || !validator.isURL(url)) { - return Promise.reject(new errors.InternalServerError({ - message: 'URL empty or invalid.', - code: 'URL_MISSING_INVALID', - context: url - })); + return Promise.reject( + new errors.InternalServerError({ + message: "URL empty or invalid.", + code: "URL_MISSING_INVALID", + context: url, + }), + ); } - if (process.env.NODE_ENV?.startsWith('test') && !Object.prototype.hasOwnProperty.call(options, 'retry')) { + if ( + process.env.NODE_ENV?.startsWith("test") && + !Object.prototype.hasOwnProperty.call(options, "retry") + ) { options.retry = { - limit: 0 + limit: 0, }; } if (!options.method && (options.body || options.json)) { - options.method = 'POST'; + options.method = "POST"; } const mergedOptions = _.merge({}, defaultOptions, options); diff --git a/packages/request/package.json b/packages/request/package.json index f5783279c..6fc9bfb52 100644 --- a/packages/request/package.json +++ b/packages/request/package.json @@ -1,32 +1,27 @@ { "name": "@tryghost/request", "version": "3.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/request" }, - "author": "Ghost Foundation", - "license": "MIT", - "main": "index.js", - "scripts": { - "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" - }, "files": [ "index.js", "lib" ], + "main": "index.js", "publishConfig": { "access": "public" }, - "devDependencies": { - "nock": "14.0.11", - "rewire": "9.0.1", - "sinon": "21.0.3" + "scripts": { + "dev": "echo \"Implement me!\"", + "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "dependencies": { "@tryghost/errors": "^3.0.3", @@ -35,5 +30,10 @@ "cacheable-lookup": "7.0.0", "got": "14.6.6", "lodash": "4.17.23" + }, + "devDependencies": { + "nock": "14.0.11", + "rewire": "9.0.1", + "sinon": "21.0.3" } } diff --git a/packages/request/test/.eslintrc.js b/packages/request/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/request/test/.eslintrc.js +++ b/packages/request/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/request/test/index.test.js b/packages/request/test/index.test.js index c08a60f39..02237c85a 100644 --- a/packages/request/test/index.test.js +++ b/packages/request/test/index.test.js @@ -1,9 +1,9 @@ -const assert = require('assert/strict'); +const assert = require("assert/strict"); -describe('Request index', function () { - it('exports request implementation', function () { - const indexExport = require('../index'); - const libExport = require('../lib/request'); +describe("Request index", function () { + it("exports request implementation", function () { + const indexExport = require("../index"); + const libExport = require("../lib/request"); assert.equal(indexExport, libExport); }); diff --git a/packages/request/test/request.test.js b/packages/request/test/request.test.js index 826ccdb38..367199c0c 100644 --- a/packages/request/test/request.test.js +++ b/packages/request/test/request.test.js @@ -1,49 +1,47 @@ -const assert = require('assert/strict'); -const nock = require('nock'); +const assert = require("assert/strict"); +const nock = require("nock"); -const request = require('../lib/request'); +const request = require("../lib/request"); -describe('Request', function () { +describe("Request", function () { it('has "safe" version in default user-agent header', function () { - const url = 'http://some-website.com/endpoint/'; + const url = "http://some-website.com/endpoint/"; - nock('http://some-website.com') - .get('/endpoint/') - .reply(200, 'Response body'); + nock("http://some-website.com").get("/endpoint/").reply(200, "Response body"); return request(url, {}).then(function (res) { - assert.match(res.request.options.headers['user-agent'], /Ghost\/[0-9]+\.[0-9]+\s/); + assert.match(res.request.options.headers["user-agent"], /Ghost\/[0-9]+\.[0-9]+\s/); }); }); - it('can be called with no options', function () { - const url = 'http://some-website.com/endpoint/'; + it("can be called with no options", function () { + const url = "http://some-website.com/endpoint/"; - const requestMock = nock('http://some-website.com') - .get('/endpoint/') - .reply(200, 'Response body'); + const requestMock = nock("http://some-website.com") + .get("/endpoint/") + .reply(200, "Response body"); return request(url).then(function () { assert.equal(requestMock.isDone(), true); }); }); - it('[success] should return response for http request', function () { - const url = 'http://some-website.com/endpoint/'; + it("[success] should return response for http request", function () { + const url = "http://some-website.com/endpoint/"; const expectedResponse = { - body: 'Response body', - url: 'http://some-website.com/endpoint/', - statusCode: 200 + body: "Response body", + url: "http://some-website.com/endpoint/", + statusCode: 200, }; const options = { headers: { - 'User-Agent': 'Mozilla/5.0' - } + "User-Agent": "Mozilla/5.0", + }, }; - const requestMock = nock('http://some-website.com') - .get('/endpoint/') - .reply(200, 'Response body'); + const requestMock = nock("http://some-website.com") + .get("/endpoint/") + .reply(200, "Response body"); return request(url, options).then(function (res) { assert.equal(requestMock.isDone(), true); @@ -57,29 +55,28 @@ describe('Request', function () { }); }); - it('[success] can handle redirect', function () { - const url = 'http://some-website.com/endpoint/'; + it("[success] can handle redirect", function () { + const url = "http://some-website.com/endpoint/"; const expectedResponse = { - body: 'Redirected response', - url: 'http://someredirectedurl.com/files/', - statusCode: 200 + body: "Redirected response", + url: "http://someredirectedurl.com/files/", + statusCode: 200, }; const options = { headers: { - 'User-Agent': 'Mozilla/5.0' - } + "User-Agent": "Mozilla/5.0", + }, }; - const requestMock = nock('http://some-website.com') - .get('/endpoint/') - .reply(301, 'Oops, got redirected', - { - location: 'http://someredirectedurl.com/files/' - }); + const requestMock = nock("http://some-website.com") + .get("/endpoint/") + .reply(301, "Oops, got redirected", { + location: "http://someredirectedurl.com/files/", + }); - const secondRequestMock = nock('http://someredirectedurl.com') - .get('/files/') - .reply(200, 'Redirected response'); + const secondRequestMock = nock("http://someredirectedurl.com") + .get("/files/") + .reply(200, "Redirected response"); return request(url, options).then(function (res) { assert.equal(requestMock.isDone(), true); @@ -94,158 +91,171 @@ describe('Request', function () { }); }); - it('[failure] can handle invalid url', function () { - const url = 'test'; + it("[failure] can handle invalid url", function () { + const url = "test"; const options = { headers: { - 'User-Agent': 'Mozilla/5.0' - } + "User-Agent": "Mozilla/5.0", + }, }; - return request(url, options).then(() => { - throw new Error('Request should have rejected with invalid url message'); - }, (err) => { - assert.notEqual(err, undefined); - assert.equal(err.message, 'URL empty or invalid.'); - }); + return request(url, options).then( + () => { + throw new Error("Request should have rejected with invalid url message"); + }, + (err) => { + assert.notEqual(err, undefined); + assert.equal(err.message, "URL empty or invalid."); + }, + ); }); - it('[failure] can handle empty url', function () { - const url = ''; + it("[failure] can handle empty url", function () { + const url = ""; const options = { headers: { - 'User-Agent': 'Mozilla/5.0' - } + "User-Agent": "Mozilla/5.0", + }, }; - return request(url, options).then(() => { - throw new Error('Request should have rejected with invalid url message'); - }, (err) => { - assert.notEqual(err, undefined); - assert.equal(err.message, 'URL empty or invalid.'); - }); + return request(url, options).then( + () => { + throw new Error("Request should have rejected with invalid url message"); + }, + (err) => { + assert.notEqual(err, undefined); + assert.equal(err.message, "URL empty or invalid."); + }, + ); }); - it('[failure] can handle an error with statuscode not 200', function () { - const url = 'http://nofilehere.com/files/test.txt'; + it("[failure] can handle an error with statuscode not 200", function () { + const url = "http://nofilehere.com/files/test.txt"; const options = { headers: { - 'User-Agent': 'Mozilla/5.0' - } + "User-Agent": "Mozilla/5.0", + }, }; - const requestMock = nock('http://nofilehere.com') - .get('/files/test.txt') - .reply(404); + const requestMock = nock("http://nofilehere.com").get("/files/test.txt").reply(404); - return request(url, options).then(() => { - throw new Error('Request should have errored'); - }, (err) => { - assert.equal(requestMock.isDone(), true); - assert.notEqual(err, undefined); - assert.equal(err.statusMessage, 'Not Found'); - }); + return request(url, options).then( + () => { + throw new Error("Request should have errored"); + }, + (err) => { + assert.equal(requestMock.isDone(), true); + assert.notEqual(err, undefined); + assert.equal(err.statusMessage, "Not Found"); + }, + ); }); - it('[failure] returns error if request errors', function () { - const url = 'http://nofilehere.com/files/test.txt'; + it("[failure] returns error if request errors", function () { + const url = "http://nofilehere.com/files/test.txt"; const options = { headers: { - 'User-Agent': 'Mozilla/5.0' + "User-Agent": "Mozilla/5.0", }, retry: { // Set delay between retries to 1ms - 2 retries total limit: 2, - backoffLimit: 1 - } + backoffLimit: 1, + }, }; - const requestMock = nock('http://nofilehere.com') - .get('/files/test.txt') + const requestMock = nock("http://nofilehere.com") + .get("/files/test.txt") .times(3) // 1 original request + 2 default retries - .reply(500, {message: 'something awful happened', code: 'AWFUL_ERROR'}); + .reply(500, { message: "something awful happened", code: "AWFUL_ERROR" }); - return request(url, options).then(() => { - throw new Error('Request should have errored with an awful error'); - }, (err) => { - assert.equal(requestMock.isDone(), true); - assert.notEqual(err, undefined); - assert.equal(err.statusMessage, 'Internal Server Error'); - assert.match(err.body, /something awful happened/); - assert.match(err.body, /AWFUL_ERROR/); - }); + return request(url, options).then( + () => { + throw new Error("Request should have errored with an awful error"); + }, + (err) => { + assert.equal(requestMock.isDone(), true); + assert.notEqual(err, undefined); + assert.equal(err.statusMessage, "Internal Server Error"); + assert.match(err.body, /something awful happened/); + assert.match(err.body, /AWFUL_ERROR/); + }, + ); }); - it('[failure] should timeout when taking too long', function () { - const url = 'http://some-website.com/endpoint/'; + it("[failure] should timeout when taking too long", function () { + const url = "http://some-website.com/endpoint/"; const options = { headers: { - 'User-Agent': 'Mozilla/5.0' + "User-Agent": "Mozilla/5.0", }, timeout: { - request: 1 + request: 1, }, retry: { - limit: 0 - } // got retries by default so we're disabling this behavior + limit: 0, + }, // got retries by default so we're disabling this behavior }; - nock('http://some-website.com') - .get('/endpoint/') - .delay(20) - .reply(200, 'Response body'); + nock("http://some-website.com").get("/endpoint/").delay(20).reply(200, "Response body"); - return request(url, options).then(() => { - throw new Error('Should have timed out'); - }, (err) => { - assert.equal(err.code, 'ETIMEDOUT'); - }); + return request(url, options).then( + () => { + throw new Error("Should have timed out"); + }, + (err) => { + assert.equal(err.code, "ETIMEDOUT"); + }, + ); }); - it('[success] defaults to POST when body is provided without method', function () { - const url = 'http://some-website.com/post-endpoint/'; - const requestMock = nock('http://some-website.com') - .post('/post-endpoint/', 'hello') - .reply(200, 'ok'); + it("[success] defaults to POST when body is provided without method", function () { + const url = "http://some-website.com/post-endpoint/"; + const requestMock = nock("http://some-website.com") + .post("/post-endpoint/", "hello") + .reply(200, "ok"); - return request(url, {body: 'hello'}).then(function (res) { + return request(url, { body: "hello" }).then(function (res) { assert.equal(requestMock.isDone(), true); assert.equal(res.statusCode, 200); }); }); - it('[success] defaults to POST when json is provided without method', function () { - const url = 'http://some-website.com/json-endpoint/'; - const payload = {hello: 'world'}; - const requestMock = nock('http://some-website.com') - .post('/json-endpoint/', payload) - .reply(200, 'ok'); + it("[success] defaults to POST when json is provided without method", function () { + const url = "http://some-website.com/json-endpoint/"; + const payload = { hello: "world" }; + const requestMock = nock("http://some-website.com") + .post("/json-endpoint/", payload) + .reply(200, "ok"); - return request(url, {json: payload}).then(function (res) { + return request(url, { json: payload }).then(function (res) { assert.equal(requestMock.isDone(), true); assert.equal(res.statusCode, 200); }); }); - it('[failure] adds request options and response fields onto thrown error', function () { - const url = 'http://some-website.com/forbidden/'; - const requestMock = nock('http://some-website.com') - .get('/forbidden/') - .reply(403, 'forbidden'); + it("[failure] adds request options and response fields onto thrown error", function () { + const url = "http://some-website.com/forbidden/"; + const requestMock = nock("http://some-website.com") + .get("/forbidden/") + .reply(403, "forbidden"); return request(url, { headers: { - 'x-test': 'yes' - } - }).then(() => { - throw new Error('Should have failed'); - }, (err) => { - assert.equal(requestMock.isDone(), true); - assert.notEqual(err.method, undefined); - assert.notEqual(err.url, undefined); - assert.equal(err.statusCode, 403); - assert.equal(err.body, 'forbidden'); - assert.notEqual(err.response, undefined); - }); + "x-test": "yes", + }, + }).then( + () => { + throw new Error("Should have failed"); + }, + (err) => { + assert.equal(requestMock.isDone(), true); + assert.notEqual(err.method, undefined); + assert.notEqual(err.url, undefined); + assert.equal(err.statusCode, 403); + assert.equal(err.body, "forbidden"); + assert.notEqual(err.response, undefined); + }, + ); }); }); diff --git a/packages/root-utils/README.md b/packages/root-utils/README.md index 40b74476a..53d1605d2 100644 --- a/packages/root-utils/README.md +++ b/packages/root-utils/README.md @@ -8,36 +8,30 @@ or `yarn add @tryghost/root-utils` - ## Purpose Utilities for resolving and working with the effective process root across Ghost packages. ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests +# Copyright & License - - -# Copyright & License - -Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/root-utils/index.js b/packages/root-utils/index.js index 9ad95b68e..00f0cb7d4 100644 --- a/packages/root-utils/index.js +++ b/packages/root-utils/index.js @@ -1 +1 @@ -module.exports = require('./lib/root-utils'); +module.exports = require("./lib/root-utils"); diff --git a/packages/root-utils/lib/root-utils.js b/packages/root-utils/lib/root-utils.js index c352fac54..eca9eb9e5 100644 --- a/packages/root-utils/lib/root-utils.js +++ b/packages/root-utils/lib/root-utils.js @@ -1,7 +1,7 @@ -const fs = require('fs'); -const path = require('path'); -const findRoot = require('find-root'); -const caller = require('caller'); +const fs = require("fs"); +const path = require("path"); +const findRoot = require("find-root"); +const caller = require("caller"); /** * @description Get root directory of caller. @@ -29,13 +29,13 @@ exports.getCallerRoot = function getCallerRoot() { * Used to find the root directory (where a package.json exists) nearest to the current * working directory of the process. This means that configuration that exists at the root * of the project can be accessed by any of the modules required by the project. - * + * * Includes logic to determine whether a `current` symlink exists in the working directory, * which will be used rather than walking up the file tree if it exists */ exports.getProcessRoot = function getProcessRoot() { let workingDirectory = process.cwd(); - const currentFolder = path.join(workingDirectory, 'current'); + const currentFolder = path.join(workingDirectory, "current"); try { const folderInfo = fs.statSync(currentFolder); if (folderInfo.isDirectory()) { @@ -44,7 +44,7 @@ exports.getProcessRoot = function getProcessRoot() { } catch (err) { // No-op - continue with normal working directory } - try { + try { return findRoot(workingDirectory); } catch (err) { return; diff --git a/packages/root-utils/package.json b/packages/root-utils/package.json index 6ac28b218..cb0c0f2df 100644 --- a/packages/root-utils/package.json +++ b/packages/root-utils/package.json @@ -1,33 +1,33 @@ { "name": "@tryghost/root-utils", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/root-utils" }, - "author": "Ghost Foundation", - "license": "MIT", - "main": "index.js", - "scripts": { - "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" - }, "files": [ "index.js", "lib" ], + "main": "index.js", "publishConfig": { "access": "public" }, - "devDependencies": { - "sinon": "21.0.3" + "scripts": { + "dev": "echo \"Implement me!\"", + "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "dependencies": { "caller": "1.1.0", "find-root": "1.1.0" + }, + "devDependencies": { + "sinon": "21.0.3" } } diff --git a/packages/root-utils/test/.eslintrc.js b/packages/root-utils/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/root-utils/test/.eslintrc.js +++ b/packages/root-utils/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/root-utils/test/root-utils.test.ts b/packages/root-utils/test/root-utils.test.ts index 5e247b822..778aa8714 100644 --- a/packages/root-utils/test/root-utils.test.ts +++ b/packages/root-utils/test/root-utils.test.ts @@ -1,10 +1,10 @@ -const assert = require('assert/strict'); -const path = require('path'); -const fs = require('fs'); -const os = require('os'); -const Module = require('module'); -const {getProcessRoot} = require('../index'); -const rootUtilsModulePath = require.resolve('../lib/root-utils'); +const assert = require("assert/strict"); +const path = require("path"); +const fs = require("fs"); +const os = require("os"); +const Module = require("module"); +const { getProcessRoot } = require("../index"); +const rootUtilsModulePath = require.resolve("../lib/root-utils"); function loadRootUtilsWithMocks(mocks) { const originalLoad = Module._load; @@ -17,68 +17,68 @@ function loadRootUtilsWithMocks(mocks) { try { delete require.cache[rootUtilsModulePath]; - return require('../lib/root-utils'); + return require("../lib/root-utils"); } finally { Module._load = originalLoad; delete require.cache[rootUtilsModulePath]; } } -describe('getCallerRoot', function () { - it('Gets the root directory of the caller', function () { +describe("getCallerRoot", function () { + it("Gets the root directory of the caller", function () { const mockedModule = loadRootUtilsWithMocks({ caller: () => __filename, - 'find-root': require('find-root') + "find-root": require("find-root"), }); const callerRoot = mockedModule.getCallerRoot(); - assert.ok(callerRoot, 'caller root should be defined'); - assert.ok(typeof callerRoot === 'string', 'caller root should be a string'); - assert.equal(callerRoot.endsWith('root-utils'), true); + assert.ok(callerRoot, "caller root should be defined"); + assert.ok(typeof callerRoot === "string", "caller root should be a string"); + assert.equal(callerRoot.endsWith("root-utils"), true); }); - it('returns undefined when caller root cannot be resolved', function () { + it("returns undefined when caller root cannot be resolved", function () { const mockedModule = loadRootUtilsWithMocks({ - caller: () => '/tmp/no-root-here.js', - 'find-root': () => { - throw new Error('no package root'); - } + caller: () => "/tmp/no-root-here.js", + "find-root": () => { + throw new Error("no package root"); + }, }); assert.equal(mockedModule.getCallerRoot(), undefined); }); }); -describe('getProcessRoot', function () { - it('Gets the `current` root directory of the process', function () { - fs.mkdirSync('current'); - fs.closeSync(fs.openSync(path.join('current', 'package.json'), 'w')); +describe("getProcessRoot", function () { + it("Gets the `current` root directory of the process", function () { + fs.mkdirSync("current"); + fs.closeSync(fs.openSync(path.join("current", "package.json"), "w")); // `current` directory contains a package.json, and is picked over `root-utils` - assert.equal(getProcessRoot().endsWith('current'), true); + assert.equal(getProcessRoot().endsWith("current"), true); - fs.unlinkSync(path.join('current', 'package.json')); - fs.rmdirSync('current'); + fs.unlinkSync(path.join("current", "package.json")); + fs.rmdirSync("current"); }); - it('Gets the root when no `current` directory exists', function () { - assert.equal(getProcessRoot().endsWith('root-utils'), true); + it("Gets the root when no `current` directory exists", function () { + assert.equal(getProcessRoot().endsWith("root-utils"), true); }); - it('ignores `current` when it exists but is not a directory', function () { - const currentPath = path.join(process.cwd(), 'current'); - fs.writeFileSync(currentPath, 'not a directory'); + it("ignores `current` when it exists but is not a directory", function () { + const currentPath = path.join(process.cwd(), "current"); + fs.writeFileSync(currentPath, "not a directory"); try { - assert.equal(getProcessRoot().endsWith('root-utils'), true); + assert.equal(getProcessRoot().endsWith("root-utils"), true); } finally { fs.unlinkSync(currentPath); } }); - it('returns undefined when no package root can be found from cwd', function () { + it("returns undefined when no package root can be found from cwd", function () { const previousCwd = process.cwd(); - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'root-utils-no-root-')); + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "root-utils-no-root-")); try { process.chdir(tempDir); diff --git a/packages/security/README.md b/packages/security/README.md index f19684519..9456d305c 100644 --- a/packages/security/README.md +++ b/packages/security/README.md @@ -17,11 +17,11 @@ or ## Usage ```js -const security = require('@tryghost/security'); +const security = require("@tryghost/security"); -const secret = security.secret.create('content'); -const hash = await security.password.hash('super-secret'); -const isMatch = await security.password.compare('super-secret', hash); +const secret = security.secret.create("content"); +const hash = await security.password.hash("super-secret"); +const isMatch = await security.password.compare("super-secret", hash); ``` ## API diff --git a/packages/security/index.js b/packages/security/index.js index b4ef42a7f..2398ce407 100644 --- a/packages/security/index.js +++ b/packages/security/index.js @@ -1,25 +1,25 @@ module.exports = { get url() { - return require('./lib/url'); + return require("./lib/url"); }, get tokens() { - return require('./lib/tokens'); + return require("./lib/tokens"); }, get string() { - return require('./lib/string'); + return require("./lib/string"); }, get identifier() { - return require('./lib/identifier'); + return require("./lib/identifier"); }, get password() { - return require('./lib/password'); + return require("./lib/password"); }, get secret() { - return require('./lib/secret'); - } + return require("./lib/secret"); + }, }; diff --git a/packages/security/lib/identifier.js b/packages/security/lib/identifier.js index f8dcc9f00..98cc6946a 100644 --- a/packages/security/lib/identifier.js +++ b/packages/security/lib/identifier.js @@ -13,7 +13,7 @@ _private.getRandomInt = function (min, max) { */ module.exports.uid = function uid(maxLength) { const buf = []; - const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; const charLength = chars.length; let i; @@ -21,5 +21,5 @@ module.exports.uid = function uid(maxLength) { buf.push(chars[_private.getRandomInt(0, charLength - 1)]); } - return buf.join(''); + return buf.join(""); }; diff --git a/packages/security/lib/password.js b/packages/security/lib/password.js index 0bf906481..7cb41c7eb 100644 --- a/packages/security/lib/password.js +++ b/packages/security/lib/password.js @@ -1,8 +1,8 @@ -const bcrypt = require('bcryptjs'); +const bcrypt = require("bcryptjs"); let HASH_ROUNDS = 10; -if (process.env.NODE_ENV?.startsWith('testing')) { +if (process.env.NODE_ENV?.startsWith("testing")) { HASH_ROUNDS = 1; } diff --git a/packages/security/lib/secret.js b/packages/security/lib/secret.js index 205e03b93..0fbeee09c 100644 --- a/packages/security/lib/secret.js +++ b/packages/security/lib/secret.js @@ -1,4 +1,4 @@ -const crypto = require('crypto'); +const crypto = require("crypto"); /* * Uses birthday problem estimation to calculate chance of collision @@ -28,7 +28,7 @@ module.exports.create = (typeOrLength) => { if (Number.isInteger(typeOrLength)) { bytes = Math.ceil(typeOrLength / 2); length = typeOrLength; - } else if (typeOrLength === 'content') { + } else if (typeOrLength === "content") { bytes = 13; length = 26; } else { @@ -36,5 +36,5 @@ module.exports.create = (typeOrLength) => { length = 64; } - return crypto.randomBytes(bytes).toString('hex').slice(0, length); + return crypto.randomBytes(bytes).toString("hex").slice(0, length); }; diff --git a/packages/security/lib/string.js b/packages/security/lib/string.js index 100fba509..33580efba 100644 --- a/packages/security/lib/string.js +++ b/packages/security/lib/string.js @@ -1,9 +1,9 @@ -const slugify = require('@tryghost/string').slugify; +const slugify = require("@tryghost/string").slugify; /** * @param {string} string * @param {{importing?: boolean}} [options] */ module.exports.safe = function safe(string, options = {}) { - return slugify(string, {requiredChangesOnly: options.importing === true}); + return slugify(string, { requiredChangesOnly: options.importing === true }); }; diff --git a/packages/security/lib/tokens.js b/packages/security/lib/tokens.js index 9614d06fe..b8a799821 100644 --- a/packages/security/lib/tokens.js +++ b/packages/security/lib/tokens.js @@ -1,66 +1,66 @@ -const crypto = require('crypto'); +const crypto = require("crypto"); module.exports.generateFromContent = function generateFromContent(options) { options = options || {}; - const hash = crypto.createHash('sha256'); + const hash = crypto.createHash("sha256"); const content = options.content; - let text = ''; + let text = ""; hash.update(content); - text += [content, hash.digest('base64')].join('|'); - return Buffer.from(text).toString('base64'); + text += [content, hash.digest("base64")].join("|"); + return Buffer.from(text).toString("base64"); }; module.exports.generateFromEmail = function generateFromEmail(options) { options = options || {}; - const hash = crypto.createHash('sha256'); + const hash = crypto.createHash("sha256"); const expires = options.expires; const email = options.email; const secret = options.secret; - let text = ''; + let text = ""; hash.update(String(expires)); hash.update(email.toLocaleLowerCase()); hash.update(String(secret)); - text += [expires, email, hash.digest('base64')].join('|'); - return Buffer.from(text).toString('base64'); + text += [expires, email, hash.digest("base64")].join("|"); + return Buffer.from(text).toString("base64"); }; module.exports.resetToken = { generateHash: function generateHash(options) { options = options || {}; - const hash = crypto.createHash('sha256'); + const hash = crypto.createHash("sha256"); const expires = options.expires; const email = options.email; const dbHash = options.dbHash; const password = options.password; - let text = ''; + let text = ""; hash.update(String(expires)); hash.update(email.toLocaleLowerCase()); hash.update(password); hash.update(String(dbHash)); - text += [expires, email, hash.digest('base64')].join('|'); - return Buffer.from(text).toString('base64'); + text += [expires, email, hash.digest("base64")].join("|"); + return Buffer.from(text).toString("base64"); }, extract: function extract(options) { options = options || {}; const token = options.token; - const tokenText = Buffer.from(token, 'base64').toString('ascii'); + const tokenText = Buffer.from(token, "base64").toString("ascii"); let parts; let expires; let email; - parts = tokenText.split('|'); + parts = tokenText.split("|"); // Check if invalid structure if (!parts || parts.length !== 3) { @@ -72,14 +72,14 @@ module.exports.resetToken = { return { expires: expires, - email: email + email: email, }; }, compare: function compare(options) { options = options || {}; const tokenToCompare = options.token; - const parts = exports.resetToken.extract({token: tokenToCompare}); + const parts = exports.resetToken.extract({ token: tokenToCompare }); const dbHash = options.dbHash; const password = options.password; let generatedToken; @@ -89,7 +89,7 @@ module.exports.resetToken = { if (isNaN(parts.expires)) { return { correct: false, - reason: 'invalid_expiry' + reason: "invalid_expiry", }; } @@ -97,7 +97,7 @@ module.exports.resetToken = { if (parts.expires < Date.now()) { return { correct: false, - reason: 'expired' + reason: "expired", }; } @@ -105,7 +105,7 @@ module.exports.resetToken = { email: parts.email, expires: parts.expires, dbHash: dbHash, - password: password + password: password, }); if (tokenToCompare.length !== generatedToken.length) { @@ -117,13 +117,13 @@ module.exports.resetToken = { } const result = { - correct: (diff === 0) + correct: diff === 0, }; if (!result.correct) { - result.reason = 'invalid'; + result.reason = "invalid"; } return result; - } + }, }; diff --git a/packages/security/lib/url.js b/packages/security/lib/url.js index 400403740..bbdb1cfa8 100644 --- a/packages/security/lib/url.js +++ b/packages/security/lib/url.js @@ -1,14 +1,14 @@ // The token is encoded URL safe by replacing '+' with '-', '\' with '_' and removing '=' // NOTE: the token is not encoded using valid base64 anymore module.exports.encodeBase64 = function encodeBase64(base64String) { - return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); + return base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); }; // Decode url safe base64 encoding and add padding ('=') module.exports.decodeBase64 = function decodeBase64(base64String) { - base64String = base64String.replace(/-/g, '+').replace(/_/g, '/'); + base64String = base64String.replace(/-/g, "+").replace(/_/g, "/"); while (base64String.length % 4) { - base64String += '='; + base64String += "="; } return base64String; }; diff --git a/packages/security/package.json b/packages/security/package.json index 55f6550cb..cd9da8f1b 100644 --- a/packages/security/package.json +++ b/packages/security/package.json @@ -2,13 +2,17 @@ "name": "@tryghost/security", "version": "3.0.3", "description": "Security primitives for token generation, password hashing, and safe identifiers", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/security" }, - "author": "Ghost Foundation", - "license": "MIT", + "files": [ + "index.js", + "lib" + ], "main": "index.js", "publishConfig": { "access": "public" @@ -22,16 +26,12 @@ "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", "lint:eslint": "yarn lint:code && yarn lint:test" }, - "files": [ - "index.js", - "lib" - ], - "devDependencies": { - "sinon": "21.0.3", - "uuid": "13.0.0" - }, "dependencies": { "@tryghost/string": "0.3.1", "bcryptjs": "3.0.3" + }, + "devDependencies": { + "sinon": "21.0.3", + "uuid": "13.0.0" } } diff --git a/packages/security/test/.eslintrc.js b/packages/security/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/security/test/.eslintrc.js +++ b/packages/security/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/security/test/identifier.test.js b/packages/security/test/identifier.test.js index 4f2fc51da..004fc8ce9 100644 --- a/packages/security/test/identifier.test.js +++ b/packages/security/test/identifier.test.js @@ -1,8 +1,8 @@ -const assert = require('assert/strict'); -const {identifier} = require('../'); +const assert = require("assert/strict"); +const { identifier } = require("../"); -describe('Lib: Security - Identifier', function () { - it('creates UID strings with requested length', function () { +describe("Lib: Security - Identifier", function () { + it("creates UID strings with requested length", function () { const uid = identifier.uid(24); assert.equal(uid.length, 24); diff --git a/packages/security/test/password.test.js b/packages/security/test/password.test.js index 707f8e4ae..9669bca02 100644 --- a/packages/security/test/password.test.js +++ b/packages/security/test/password.test.js @@ -1,16 +1,16 @@ -const assert = require('assert/strict'); -const security = require('../'); +const assert = require("assert/strict"); +const security = require("../"); -describe('Lib: Security - Password', function () { - it('hash plain password', function () { - return security.password.hash('test') - .then(function (hash) { - assert.match(hash, /^\$2[ayb]\$.{56}$/); - }); +describe("Lib: Security - Password", function () { + it("hash plain password", function () { + return security.password.hash("test").then(function (hash) { + assert.match(hash, /^\$2[ayb]\$.{56}$/); + }); }); - it('compare password', function () { - return security.password.compare('test', '$2a$10$we16f8rpbrFZ34xWj0/ZC.LTPUux8ler7bcdTs5qIleN6srRHhilG') + it("compare password", function () { + return security.password + .compare("test", "$2a$10$we16f8rpbrFZ34xWj0/ZC.LTPUux8ler7bcdTs5qIleN6srRHhilG") .then(function (valid) { assert.equal(valid, true); }); diff --git a/packages/security/test/secret.test.js b/packages/security/test/secret.test.js index 1a806582f..2a78a364e 100644 --- a/packages/security/test/secret.test.js +++ b/packages/security/test/secret.test.js @@ -1,38 +1,38 @@ -const assert = require('assert/strict'); -const security = require('../'); +const assert = require("assert/strict"); +const security = require("../"); -describe('Lib: Security - Secret', function () { - it('generates a 13 byte secret if asked for a content secret', function () { - let secret = security.secret.create('content'); - assert.equal(typeof secret, 'string'); +describe("Lib: Security - Secret", function () { + it("generates a 13 byte secret if asked for a content secret", function () { + let secret = security.secret.create("content"); + assert.equal(typeof secret, "string"); assert.equal(secret.length, 13 * 2); assert.match(secret, /[0-9a-z]+/); }); - it('generates a specific length secret if given a length', function () { + it("generates a specific length secret if given a length", function () { let secret = security.secret.create(10); - assert.equal(typeof secret, 'string'); + assert.equal(typeof secret, "string"); assert.equal(secret.length, 10); assert.match(secret, /[0-9a-z]+/); }); - it('generates a specific length secret if given a length even when odd', function () { + it("generates a specific length secret if given a length even when odd", function () { let secret = security.secret.create(15); - assert.equal(typeof secret, 'string'); + assert.equal(typeof secret, "string"); assert.equal(secret.length, 15); assert.match(secret, /[0-9a-z]+/); }); - it('generates a 32 byte secret if asked for an admin secret', function () { - let secret = security.secret.create('admin'); - assert.equal(typeof secret, 'string'); + it("generates a 32 byte secret if asked for an admin secret", function () { + let secret = security.secret.create("admin"); + assert.equal(typeof secret, "string"); assert.equal(secret.length, 32 * 2); assert.match(secret, /[0-9a-z]+/); }); - it('generates a 32 byte secret by default', function () { + it("generates a 32 byte secret by default", function () { let secret = security.secret.create(); - assert.equal(typeof secret, 'string'); + assert.equal(typeof secret, "string"); assert.equal(secret.length, 32 * 2); assert.match(secret, /[0-9a-z]+/); }); diff --git a/packages/security/test/string.test.js b/packages/security/test/string.test.js index 6f05cf885..97dd72a95 100644 --- a/packages/security/test/string.test.js +++ b/packages/security/test/string.test.js @@ -1,90 +1,110 @@ -const assert = require('assert/strict'); -const security = require('../'); +const assert = require("assert/strict"); +const security = require("../"); -describe('Lib: Security - String', function () { - describe('Safe String', function () { +describe("Lib: Security - String", function () { + describe("Safe String", function () { const options = {}; - it('should remove beginning and ending whitespace', function () { - const result = security.string.safe(' stringwithspace ', options); - assert.equal(result, 'stringwithspace'); + it("should remove beginning and ending whitespace", function () { + const result = security.string.safe(" stringwithspace ", options); + assert.equal(result, "stringwithspace"); }); - it('can handle null strings', function () { + it("can handle null strings", function () { const result = security.string.safe(null); - assert.equal(result, ''); + assert.equal(result, ""); }); - it('should remove non ascii characters', function () { - const result = security.string.safe('howtowin✓', options); - assert.equal(result, 'howtowin'); + it("should remove non ascii characters", function () { + const result = security.string.safe("howtowin✓", options); + assert.equal(result, "howtowin"); }); - it('should replace spaces with dashes', function () { - const result = security.string.safe('how to win', options); - assert.equal(result, 'how-to-win'); + it("should replace spaces with dashes", function () { + const result = security.string.safe("how to win", options); + assert.equal(result, "how-to-win"); }); - it('should replace most special characters with dashes', function () { - const result = security.string.safe('a:b/c?d#e[f]g!h$i&j(k)l*m+n,o;{p}=q\\r%su|v^w~x£y"z@1.2`3', options); - assert.equal(result, 'a-b-c-d-e-f-g-h-i-j-k-l-m-n-o-p-q-r-s-t-u-v-w-x-y-z-1-2-3'); + it("should replace most special characters with dashes", function () { + const result = security.string.safe( + 'a:b/c?d#e[f]g!h$i&j(k)l*m+n,o;{p}=q\\r%su|v^w~x£y"z@1.2`3', + options, + ); + assert.equal(result, "a-b-c-d-e-f-g-h-i-j-k-l-m-n-o-p-q-r-s-t-u-v-w-x-y-z-1-2-3"); }); - it('should replace all of the html4 compat symbols in ascii except hyphen and underscore', function () { + it("should replace all of the html4 compat symbols in ascii except hyphen and underscore", function () { // note: This is missing the soft-hyphen char that isn't much-liked by linters/browsers/etc, // it passed the test before it was removed - const result = security.string.safe('!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿'); - assert.equal(result, '_-c-y-ss-c-a-r-deg-23up-1o-1-41-23-4'); + const result = security.string.safe( + "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿", + ); + assert.equal(result, "_-c-y-ss-c-a-r-deg-23up-1o-1-41-23-4"); }); - it('should replace all of the foreign chars in ascii', function () { - const result = security.string.safe('ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ'); - assert.equal(result, 'aaaaaaaeceeeeiiiidnoooooxouuuuythssaaaaaaaeceeeeiiiidnooooo-ouuuuythy'); + it("should replace all of the foreign chars in ascii", function () { + const result = security.string.safe( + "ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ", + ); + assert.equal( + result, + "aaaaaaaeceeeeiiiidnoooooxouuuuythssaaaaaaaeceeeeiiiidnooooo-ouuuuythy", + ); }); - it('should remove special characters at the beginning of a string', function () { - const result = security.string.safe('.Not special', options); - assert.equal(result, 'not-special'); + it("should remove special characters at the beginning of a string", function () { + const result = security.string.safe(".Not special", options); + assert.equal(result, "not-special"); }); - it('should remove apostrophes ', function () { - const result = security.string.safe('how we shouldn\'t be', options); - assert.equal(result, 'how-we-shouldnt-be'); + it("should remove apostrophes ", function () { + const result = security.string.safe("how we shouldn't be", options); + assert.equal(result, "how-we-shouldnt-be"); }); - it('should convert to lowercase', function () { - const result = security.string.safe('This has Upper Case', options); - assert.equal(result, 'this-has-upper-case'); + it("should convert to lowercase", function () { + const result = security.string.safe("This has Upper Case", options); + assert.equal(result, "this-has-upper-case"); }); - it('should convert multiple dashes into a single dash', function () { - const result = security.string.safe('This :) means everything', options); - assert.equal(result, 'this-means-everything'); + it("should convert multiple dashes into a single dash", function () { + const result = security.string.safe("This :) means everything", options); + assert.equal(result, "this-means-everything"); }); - it('should remove trailing dashes from the result', function () { - const result = security.string.safe('This.', options); - assert.equal(result, 'this'); + it("should remove trailing dashes from the result", function () { + const result = security.string.safe("This.", options); + assert.equal(result, "this"); }); - it('should handle pound signs', function () { - const result = security.string.safe('WHOOPS! I spent all my £ again!', options); - assert.equal(result, 'whoops-i-spent-all-my-again'); + it("should handle pound signs", function () { + const result = security.string.safe("WHOOPS! I spent all my £ again!", options); + assert.equal(result, "whoops-i-spent-all-my-again"); }); - it('should properly handle unicode punctuation conversion', function () { - const result = security.string.safe('に間違いがないか、再度確認してください。再読み込みしてください。', options); - assert.equal(result, 'nijian-wei-iganaika-zai-du-que-ren-sitekudasai-zai-du-miip-misitekudasai'); + it("should properly handle unicode punctuation conversion", function () { + const result = security.string.safe( + "に間違いがないか、再度確認してください。再読み込みしてください。", + options, + ); + assert.equal( + result, + "nijian-wei-iganaika-zai-du-que-ren-sitekudasai-zai-du-miip-misitekudasai", + ); }); - it('should not lose or convert dashes if options are passed with truthy importing flag', function () { - let result = security.string.safe('-slug-with-starting-ending-and---multiple-dashes-', {importing: true}); - assert.equal(result, '-slug-with-starting-ending-and---multiple-dashes-'); + it("should not lose or convert dashes if options are passed with truthy importing flag", function () { + let result = security.string.safe("-slug-with-starting-ending-and---multiple-dashes-", { + importing: true, + }); + assert.equal(result, "-slug-with-starting-ending-and---multiple-dashes-"); }); - it('should still remove/convert invalid characters when passed options with truthy importing flag', function () { - let result = security.string.safe('-slug-&with-✓-invalid-characters-に\'', {importing: true}); - assert.equal(result, '-slug--with--invalid-characters-ni'); + it("should still remove/convert invalid characters when passed options with truthy importing flag", function () { + let result = security.string.safe("-slug-&with-✓-invalid-characters-に'", { + importing: true, + }); + assert.equal(result, "-slug--with--invalid-characters-ni"); }); }); }); diff --git a/packages/security/test/tokens.test.js b/packages/security/test/tokens.test.js index e63be11e1..891445ddf 100644 --- a/packages/security/test/tokens.test.js +++ b/packages/security/test/tokens.test.js @@ -1,9 +1,9 @@ -const assert = require('assert/strict'); -const crypto = require('crypto'); -const security = require('../'); +const assert = require("assert/strict"); +const crypto = require("crypto"); +const security = require("../"); -describe('Utils: tokens', function () { - it('covers default options branches for token helpers', function () { +describe("Utils: tokens", function () { + it("covers default options branches for token helpers", function () { assert.throws(() => security.tokens.generateFromContent()); assert.throws(() => security.tokens.generateFromEmail()); assert.throws(() => security.tokens.resetToken.generateHash()); @@ -11,156 +11,156 @@ describe('Utils: tokens', function () { assert.throws(() => security.tokens.resetToken.compare()); }); - it('generateFromContent creates encoded content+hash token', function () { - const token = security.tokens.generateFromContent({content: 'abc'}); - const decoded = Buffer.from(token, 'base64').toString('ascii'); - const parts = decoded.split('|'); + it("generateFromContent creates encoded content+hash token", function () { + const token = security.tokens.generateFromContent({ content: "abc" }); + const decoded = Buffer.from(token, "base64").toString("ascii"); + const parts = decoded.split("|"); assert.equal(parts.length, 2); - assert.equal(parts[0], 'abc'); + assert.equal(parts[0], "abc"); assert.equal(parts[1].length > 0, true); }); - it('generateFromEmail creates encoded expires+email+hash token', function () { + it("generateFromEmail creates encoded expires+email+hash token", function () { const expires = Date.now() + 60 * 1000; const token = security.tokens.generateFromEmail({ expires, - email: 'test@example.com', - secret: 's3cr3t' + email: "test@example.com", + secret: "s3cr3t", }); - const decoded = Buffer.from(token, 'base64').toString('ascii'); - const parts = decoded.split('|'); + const decoded = Buffer.from(token, "base64").toString("ascii"); + const parts = decoded.split("|"); assert.equal(parts.length, 3); assert.equal(parts[0], String(expires)); - assert.equal(parts[1], 'test@example.com'); + assert.equal(parts[1], "test@example.com"); assert.equal(parts[2].length > 0, true); }); - it('generate', function () { + it("generate", function () { const expires = Date.now() + 60 * 1000; const dbHash = crypto.randomUUID(); let token; token = security.tokens.resetToken.generateHash({ - email: 'test1@ghost.org', + email: "test1@ghost.org", expires: expires, - password: 'password', - dbHash: dbHash + password: "password", + dbHash: dbHash, }); assert.notEqual(token, undefined); assert.equal(token.length > 0, true); }); - it('compare: success', function () { + it("compare: success", function () { const expires = Date.now() + 60 * 1000; const dbHash = crypto.randomUUID(); let token; let tokenIsCorrect; token = security.tokens.resetToken.generateHash({ - email: 'test1@ghost.org', + email: "test1@ghost.org", expires: expires, - password: '12345678', - dbHash: dbHash + password: "12345678", + dbHash: dbHash, }); tokenIsCorrect = security.tokens.resetToken.compare({ token: token, dbHash: dbHash, - password: '12345678' + password: "12345678", }); assert.equal(tokenIsCorrect.correct, true); assert.equal(tokenIsCorrect.reason, undefined); }); - it('compare: error from invalid password', function () { + it("compare: error from invalid password", function () { const expires = Date.now() + 60 * 1000; const dbHash = crypto.randomUUID(); let token; let tokenIsCorrect; token = security.tokens.resetToken.generateHash({ - email: 'test1@ghost.org', + email: "test1@ghost.org", expires: expires, - password: '12345678', - dbHash: dbHash + password: "12345678", + dbHash: dbHash, }); tokenIsCorrect = security.tokens.resetToken.compare({ token: token, dbHash: dbHash, - password: '123456' + password: "123456", }); assert.equal(tokenIsCorrect.correct, false); - assert.equal(tokenIsCorrect.reason, 'invalid'); + assert.equal(tokenIsCorrect.reason, "invalid"); }); - it('compare: error from invalid expires parameter', function () { - const invalidDate = 'not a date'; + it("compare: error from invalid expires parameter", function () { + const invalidDate = "not a date"; const dbHash = crypto.randomUUID(); let token; let tokenIsCorrect; token = security.tokens.resetToken.generateHash({ - email: 'test1@ghost.org', + email: "test1@ghost.org", expires: invalidDate, - password: '12345678', - dbHash: dbHash + password: "12345678", + dbHash: dbHash, }); tokenIsCorrect = security.tokens.resetToken.compare({ token: token, dbHash: dbHash, - password: '123456' + password: "123456", }); assert.equal(tokenIsCorrect.correct, false); - assert.equal(tokenIsCorrect.reason, 'invalid_expiry'); + assert.equal(tokenIsCorrect.reason, "invalid_expiry"); }); - it('compare: error from expired token', function () { + it("compare: error from expired token", function () { const dateInThePast = Date.now() - 60 * 1000; const dbHash = crypto.randomUUID(); let token; let tokenIsCorrect; token = security.tokens.resetToken.generateHash({ - email: 'test1@ghost.org', + email: "test1@ghost.org", expires: dateInThePast, - password: '12345678', - dbHash: dbHash + password: "12345678", + dbHash: dbHash, }); tokenIsCorrect = security.tokens.resetToken.compare({ token: token, dbHash: dbHash, - password: '123456' + password: "123456", }); assert.equal(tokenIsCorrect.correct, false); - assert.equal(tokenIsCorrect.reason, 'expired'); + assert.equal(tokenIsCorrect.reason, "expired"); }); - it('extract', function () { + it("extract", function () { const expires = Date.now() + 60 * 1000; const dbHash = crypto.randomUUID(); let token; let parts; - const email = 'test1@ghost.org'; + const email = "test1@ghost.org"; token = security.tokens.resetToken.generateHash({ email: email, expires: expires, - password: '12345678', - dbHash: dbHash + password: "12345678", + dbHash: dbHash, }); parts = security.tokens.resetToken.extract({ - token: token + token: token, }); assert.equal(parts.email, email); @@ -169,22 +169,22 @@ describe('Utils: tokens', function () { assert.equal(parts.dbHash, undefined); }); - it('extract - hashed password', function () { + it("extract - hashed password", function () { const expires = Date.now() + 60 * 1000; const dbHash = crypto.randomUUID(); let token; let parts; - const email = 'test3@ghost.org'; + const email = "test3@ghost.org"; token = security.tokens.resetToken.generateHash({ email: email, expires: expires, - password: '$2a$10$t5dY1uRRdjvqfNlXhae3uuc0nuhi.Rd7/K/9JaHHwSkLm6UUa3NsW', - dbHash: dbHash + password: "$2a$10$t5dY1uRRdjvqfNlXhae3uuc0nuhi.Rd7/K/9JaHHwSkLm6UUa3NsW", + dbHash: dbHash, }); parts = security.tokens.resetToken.extract({ - token: token + token: token, }); assert.equal(parts.email, email); @@ -193,16 +193,16 @@ describe('Utils: tokens', function () { assert.equal(parts.dbHash, undefined); }); - it('extract returns false for invalid token structure', function () { - const invalidToken = Buffer.from('one|two').toString('base64'); - const result = security.tokens.resetToken.extract({token: invalidToken}); + it("extract returns false for invalid token structure", function () { + const invalidToken = Buffer.from("one|two").toString("base64"); + const result = security.tokens.resetToken.extract({ token: invalidToken }); assert.equal(result, false); }); - it('can validate an URI encoded reset token', function () { + it("can validate an URI encoded reset token", function () { const expires = Date.now() + 60 * 1000; - const email = 'test1@ghost.org'; + const email = "test1@ghost.org"; const dbHash = crypto.randomUUID(); let token; let tokenIsCorrect; @@ -211,8 +211,8 @@ describe('Utils: tokens', function () { token = security.tokens.resetToken.generateHash({ email: email, expires: expires, - password: '12345678', - dbHash: dbHash + password: "12345678", + dbHash: dbHash, }); token = security.url.encodeBase64(token); @@ -221,7 +221,7 @@ describe('Utils: tokens', function () { token = security.url.decodeBase64(token); parts = security.tokens.resetToken.extract({ - token: token + token: token, }); assert.equal(parts.email, email); @@ -230,21 +230,21 @@ describe('Utils: tokens', function () { tokenIsCorrect = security.tokens.resetToken.compare({ token: token, dbHash: dbHash, - password: '12345678' + password: "12345678", }); assert.equal(tokenIsCorrect.correct, true); }); - it('compare treats mismatched token length as invalid', function () { + it("compare treats mismatched token length as invalid", function () { const expires = Date.now() + 60 * 1000; const dbHash = crypto.randomUUID(); const token = security.tokens.resetToken.generateHash({ - email: 'test4@ghost.org', + email: "test4@ghost.org", expires, - password: '12345678', - dbHash + password: "12345678", + dbHash, }); const mismatchedLengthToken = `${token}A`; @@ -252,10 +252,10 @@ describe('Utils: tokens', function () { const tokenIsCorrect = security.tokens.resetToken.compare({ token: mismatchedLengthToken, dbHash, - password: '12345678' + password: "12345678", }); assert.equal(tokenIsCorrect.correct, false); - assert.equal(tokenIsCorrect.reason, 'invalid'); + assert.equal(tokenIsCorrect.reason, "invalid"); }); }); diff --git a/packages/security/test/url.test.js b/packages/security/test/url.test.js index 52e0af035..54a1b4916 100644 --- a/packages/security/test/url.test.js +++ b/packages/security/test/url.test.js @@ -1,15 +1,15 @@ -const assert = require('assert/strict'); -const {url} = require('../'); +const assert = require("assert/strict"); +const { url } = require("../"); -describe('Lib: Security - URL', function () { - it('encodes and decodes URL-safe base64 values', function () { - const original = 'YWJjKysvLz0='; // abc++//= +describe("Lib: Security - URL", function () { + it("encodes and decodes URL-safe base64 values", function () { + const original = "YWJjKysvLz0="; // abc++//= const encoded = url.encodeBase64(original); const decoded = url.decodeBase64(encoded); - assert.equal(encoded.includes('+'), false); - assert.equal(encoded.includes('/'), false); - assert.equal(encoded.includes('='), false); + assert.equal(encoded.includes("+"), false); + assert.equal(encoded.includes("/"), false); + assert.equal(encoded.includes("="), false); assert.equal(decoded, original); }); }); diff --git a/packages/server/README.md b/packages/server/README.md index 6a11521ad..ef15c3334 100644 --- a/packages/server/README.md +++ b/packages/server/README.md @@ -8,36 +8,30 @@ or `yarn add @tryghost/server` - ## Purpose HTTP server lifecycle helpers for starting/stopping servers and handling listen errors consistently. ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests +# Copyright & License - - -# Copyright & License - -Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/server/index.js b/packages/server/index.js index 03ce247bf..9164f12f3 100644 --- a/packages/server/index.js +++ b/packages/server/index.js @@ -1 +1 @@ -module.exports = require('./lib/server'); +module.exports = require("./lib/server"); diff --git a/packages/server/lib/server.js b/packages/server/lib/server.js index fb7838a67..5bc4d3362 100644 --- a/packages/server/lib/server.js +++ b/packages/server/lib/server.js @@ -1,6 +1,6 @@ -const debug = require('@tryghost/debug')('server'); -const logging = require('@tryghost/logging'); -const http = require('http'); +const debug = require("@tryghost/debug")("server"); +const logging = require("@tryghost/logging"); +const http = require("http"); let server; let normalizedPort; @@ -24,10 +24,10 @@ const start = function start(app, port) { * Listen on provided port, on all network interfaces. */ - app.set('port', normalizedPort); + app.set("port", normalizedPort); server.listen(normalizedPort); - server.on('error', onError); - server.on('listening', onListening); + server.on("error", onError); + server.on("listening", onListening); return server; }; @@ -78,26 +78,25 @@ function normalizePort(val) { * @param {Error} error */ function onError(error) { - if (error.syscall !== 'listen') { + if (error.syscall !== "listen") { throw error; } - const bind = typeof normalizedPort === 'string' - ? `Pipe ${normalizedPort}` - : `Port ${normalizedPort}`; + const bind = + typeof normalizedPort === "string" ? `Pipe ${normalizedPort}` : `Port ${normalizedPort}`; // handle specific listen errors with friendly messages switch (error.code) { - case 'EACCES': - logging.error(`${bind} requires elevated privileges`); - process.exit(1); - break; - case 'EADDRINUSE': - logging.error(`${bind} is already in use`); - process.exit(1); - break; - default: - throw error; + case "EACCES": + logging.error(`${bind} requires elevated privileges`); + process.exit(1); + break; + case "EADDRINUSE": + logging.error(`${bind} is already in use`); + process.exit(1); + break; + default: + throw error; } } @@ -106,12 +105,10 @@ function onError(error) { */ function onListening() { const addr = server.address(); - const bind = typeof addr === 'string' - ? 'pipe ' + addr - : 'port ' + addr.port; - debug('Server ready'); + const bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port; + debug("Server ready"); logging.info(`Listening on ${bind} \n`); } module.exports.start = start; -module.exports.stop = stop; \ No newline at end of file +module.exports.stop = stop; diff --git a/packages/server/package.json b/packages/server/package.json index 1ead50c40..5d5f84f1c 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,33 +1,33 @@ { "name": "@tryghost/server", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/server" }, - "author": "Ghost Foundation", - "license": "MIT", - "main": "index.js", - "scripts": { - "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" - }, "files": [ "index.js", "lib" ], + "main": "index.js", "publishConfig": { "access": "public" }, - "devDependencies": { - "sinon": "21.0.3" + "scripts": { + "dev": "echo \"Implement me!\"", + "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "dependencies": { "@tryghost/debug": "^2.0.3", "@tryghost/logging": "^4.0.3" + }, + "devDependencies": { + "sinon": "21.0.3" } } diff --git a/packages/server/test/.eslintrc.js b/packages/server/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/server/test/.eslintrc.js +++ b/packages/server/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/server/test/index.test.js b/packages/server/test/index.test.js index eccdc5388..a50734953 100644 --- a/packages/server/test/index.test.js +++ b/packages/server/test/index.test.js @@ -1,9 +1,9 @@ -const assert = require('assert/strict'); +const assert = require("assert/strict"); -describe('Server index', function () { - it('exports server implementation', function () { - const indexExport = require('../index'); - const libExport = require('../lib/server'); +describe("Server index", function () { + it("exports server implementation", function () { + const indexExport = require("../index"); + const libExport = require("../lib/server"); assert.equal(indexExport, libExport); }); diff --git a/packages/server/test/server.test.js b/packages/server/test/server.test.js index e94baecd6..7c3e0fe7b 100644 --- a/packages/server/test/server.test.js +++ b/packages/server/test/server.test.js @@ -1,15 +1,15 @@ -const sinon = require('sinon'); -const assert = require('assert/strict'); +const sinon = require("sinon"); +const assert = require("assert/strict"); const sandbox = sinon.createSandbox(); -const logging = require('@tryghost/logging'); -const EventEmitter = require('events'); -const http = require('http'); -const process = require('process'); +const logging = require("@tryghost/logging"); +const EventEmitter = require("events"); +const http = require("http"); +const process = require("process"); -const server = require('../lib/server'); +const server = require("../lib/server"); -describe('Server Utils', function () { +describe("Server Utils", function () { // TODO: Mock http.createServer(app) to return { listen: (port) => {} } with logic for tests in there // TODO: Add setTimeout(() => {this.emit('error', err)}, 0) to server mock to test error handling @@ -17,80 +17,89 @@ describe('Server Utils', function () { sandbox.restore(); }); - it('Normalises port number correctly', async function () { + it("Normalises port number correctly", async function () { const testPort = 180; await new Promise((resolve) => { - sandbox.stub(http, 'createServer').callsFake(function () { + sandbox.stub(http, "createServer").callsFake(function () { return { listen: function (port) { assert.equal(port, testPort); resolve(); - } + }, }; }); - server.start({ - set: () => {} - }, testPort.toString()); + server.start( + { + set: () => {}, + }, + testPort.toString(), + ); }); }); - it('Normalises named pipe correctly', async function () { - const testPipe = 'hello'; + it("Normalises named pipe correctly", async function () { + const testPipe = "hello"; await new Promise((resolve) => { - sandbox.stub(http, 'createServer').callsFake(function () { + sandbox.stub(http, "createServer").callsFake(function () { return { listen: function (port) { assert.equal(port, testPipe); resolve(); - } + }, }; }); - server.start({ - set: () => {} - }, testPipe); + server.start( + { + set: () => {}, + }, + testPipe, + ); }); }); - it('Normalises negative port value correctly', async function () { + it("Normalises negative port value correctly", async function () { const testPort = -80; await new Promise((resolve) => { - sandbox.stub(http, 'createServer').callsFake(function () { + sandbox.stub(http, "createServer").callsFake(function () { return { listen: function (port) { assert.equal(port, false); resolve(); - } + }, }; }); - server.start({ - set: () => {} - }, testPort.toString()); + server.start( + { + set: () => {}, + }, + testPort.toString(), + ); }); }); - it('Emits listening event', async function () { - const testAddress = 'hello'; + it("Emits listening event", async function () { + const testAddress = "hello"; await new Promise((resolve) => { - sandbox.stub(logging, 'info').callsFake(function (message) { + sandbox.stub(logging, "info").callsFake(function (message) { assert.equal(message.startsWith(`Listening on pipe ${testAddress}`), true); resolve(); }); - sandbox.stub(http, 'createServer').callsFake(function () { + sandbox.stub(http, "createServer").callsFake(function () { class Server extends EventEmitter { constructor() { super(); } listen() { setTimeout(() => { - this.emit('listening'); + this.emit("listening"); }, 0); } address() { @@ -101,34 +110,39 @@ describe('Server Utils', function () { return new Server(); }); - server.start({ - set: () => {} - }, 180); + server.start( + { + set: () => {}, + }, + 180, + ); }); }); - it('Emits nice error for EACCES', async function () { + it("Emits nice error for EACCES", async function () { const testPort = 180; await new Promise((resolve) => { - sandbox.stub(process, 'exit').callsFake(function () { - }); + sandbox.stub(process, "exit").callsFake(function () {}); - sandbox.stub(logging, 'error').callsFake(function (message) { - assert.equal(message.startsWith(`Port ${testPort} requires elevated privileges`), true); + sandbox.stub(logging, "error").callsFake(function (message) { + assert.equal( + message.startsWith(`Port ${testPort} requires elevated privileges`), + true, + ); resolve(); }); - sandbox.stub(http, 'createServer').callsFake(function () { + sandbox.stub(http, "createServer").callsFake(function () { class Server extends EventEmitter { constructor() { super(); } listen() { setTimeout(() => { - this.emit('error', { - code: 'EACCES', - syscall: 'listen' + this.emit("error", { + code: "EACCES", + syscall: "listen", }); }, 0); } @@ -137,34 +151,36 @@ describe('Server Utils', function () { return new Server(); }); - server.start({ - set: () => {} - }, testPort); + server.start( + { + set: () => {}, + }, + testPort, + ); }); }); - it('Emits nice error for EADDRINUSE', async function () { + it("Emits nice error for EADDRINUSE", async function () { const testPort = 180; await new Promise((resolve) => { - sandbox.stub(process, 'exit').callsFake(function () { - }); + sandbox.stub(process, "exit").callsFake(function () {}); - sandbox.stub(logging, 'error').callsFake(function (message) { + sandbox.stub(logging, "error").callsFake(function (message) { assert.equal(message.startsWith(`Port ${testPort} is already in use`), true); resolve(); }); - sandbox.stub(http, 'createServer').callsFake(function () { + sandbox.stub(http, "createServer").callsFake(function () { class Server extends EventEmitter { constructor() { super(); } listen() { setTimeout(() => { - this.emit('error', { - code: 'EADDRINUSE', - syscall: 'listen' + this.emit("error", { + code: "EADDRINUSE", + syscall: "listen", }); }, 0); } @@ -173,29 +189,35 @@ describe('Server Utils', function () { return new Server(); }); - server.start({ - set: () => {} - }, testPort); + server.start( + { + set: () => {}, + }, + testPort, + ); }); }); - it('Emits pipe-specific error message when bound to a named pipe', async function () { - const testPipe = 'server-pipe'; + it("Emits pipe-specific error message when bound to a named pipe", async function () { + const testPipe = "server-pipe"; await new Promise((resolve) => { - sandbox.stub(process, 'exit').callsFake(function () {}); - sandbox.stub(logging, 'error').callsFake(function (message) { - assert.equal(message.startsWith(`Pipe ${testPipe} requires elevated privileges`), true); + sandbox.stub(process, "exit").callsFake(function () {}); + sandbox.stub(logging, "error").callsFake(function (message) { + assert.equal( + message.startsWith(`Pipe ${testPipe} requires elevated privileges`), + true, + ); resolve(); }); - sandbox.stub(http, 'createServer').callsFake(function () { + sandbox.stub(http, "createServer").callsFake(function () { class Server extends EventEmitter { listen() { setTimeout(() => { - this.emit('error', { - code: 'EACCES', - syscall: 'listen' + this.emit("error", { + code: "EACCES", + syscall: "listen", }); }, 0); } @@ -204,20 +226,22 @@ describe('Server Utils', function () { return new Server(); }); - server.start({ - set: () => {} - }, testPipe); + server.start( + { + set: () => {}, + }, + testPipe, + ); }); }); - it('Stops server without throwing', function () { - sandbox.stub(http, 'createServer').callsFake(function () { + it("Stops server without throwing", function () { + sandbox.stub(http, "createServer").callsFake(function () { class Server extends EventEmitter { constructor() { super(); } - listen() { - } + listen() {} close() { throw new Error(); } @@ -225,53 +249,59 @@ describe('Server Utils', function () { return new Server(); }); - server.start({ - set: () => {} - }, 180); + server.start( + { + set: () => {}, + }, + 180, + ); assert.doesNotThrow(server.stop); }); - it('Emits listening event with numeric port', async function () { + it("Emits listening event with numeric port", async function () { const testPort = 191; await new Promise((resolve) => { - sandbox.stub(logging, 'info').callsFake(function (message) { + sandbox.stub(logging, "info").callsFake(function (message) { assert.equal(message.startsWith(`Listening on port ${testPort}`), true); resolve(); }); - sandbox.stub(http, 'createServer').callsFake(function () { + sandbox.stub(http, "createServer").callsFake(function () { class Server extends EventEmitter { listen() { setTimeout(() => { - this.emit('listening'); + this.emit("listening"); }, 0); } address() { - return {port: testPort}; + return { port: testPort }; } } return new Server(); }); - server.start({ - set: () => {} - }, testPort); + server.start( + { + set: () => {}, + }, + testPort, + ); }); }); - it('Throws unknown listen errors', async function () { + it("Throws unknown listen errors", async function () { await new Promise((resolve) => { - sandbox.stub(http, 'createServer').callsFake(function () { + sandbox.stub(http, "createServer").callsFake(function () { class Server extends EventEmitter { listen() { setTimeout(() => { assert.throws(() => { - this.emit('error', { - code: 'EOTHER', - syscall: 'listen' + this.emit("error", { + code: "EOTHER", + syscall: "listen", }); }); resolve(); @@ -282,22 +312,25 @@ describe('Server Utils', function () { return new Server(); }); - server.start({ - set: () => {} - }, 180); + server.start( + { + set: () => {}, + }, + 180, + ); }); }); - it('Throws errors not originating from listen syscall', async function () { + it("Throws errors not originating from listen syscall", async function () { await new Promise((resolve) => { - sandbox.stub(http, 'createServer').callsFake(function () { + sandbox.stub(http, "createServer").callsFake(function () { class Server extends EventEmitter { listen() { setTimeout(() => { assert.throws(() => { - this.emit('error', { - code: 'EACCES', - syscall: 'not-listen' + this.emit("error", { + code: "EACCES", + syscall: "not-listen", }); }); resolve(); @@ -308,9 +341,12 @@ describe('Server Utils', function () { return new Server(); }); - server.start({ - set: () => {} - }, 180); + server.start( + { + set: () => {}, + }, + 180, + ); }); }); }); diff --git a/packages/tpl/README.md b/packages/tpl/README.md index 26c31e91c..a56cbc2a3 100644 --- a/packages/tpl/README.md +++ b/packages/tpl/README.md @@ -8,7 +8,6 @@ or `yarn add @tryghost/tpl` - ## Purpose String templating helper for replacing `{token}` placeholders with runtime values. @@ -24,34 +23,29 @@ messages = { console.error(tpl(messages.myError, {something: 'The thing'})); ``` -* Takes strings like 'Your site is now available on {url}' and interpolates them with passed in data -* Will ignore double or triple braces like {{get}} or {{{content}}} -* Can handle escaped braces e.g. \\\\{\\\\{{helpername}\\\\}\\\\} -* There's a simple bare minimum escaping needed to make {{{helpername}}} work with interpolation e.g. {\\\\{{helpername}}} - +- Takes strings like 'Your site is now available on {url}' and interpolates them with passed in data +- Will ignore double or triple braces like {{get}} or {{{content}}} +- Can handle escaped braces e.g. \\\\{\\\\{{helpername}\\\\}\\\\} +- There's a simple bare minimum escaping needed to make {{{helpername}}} work with interpolation e.g. {\\\\{{helpername}}} ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests - - - # Copyright & License Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/tpl/index.js b/packages/tpl/index.js index bcbd8ac9b..237cff51c 100644 --- a/packages/tpl/index.js +++ b/packages/tpl/index.js @@ -1 +1 @@ -module.exports = require('./lib/tpl'); +module.exports = require("./lib/tpl"); diff --git a/packages/tpl/lib/tpl.js b/packages/tpl/lib/tpl.js index 5c13f318a..b7381d4c6 100644 --- a/packages/tpl/lib/tpl.js +++ b/packages/tpl/lib/tpl.js @@ -18,7 +18,7 @@ module.exports = (string, data) => { } // We replace any escaped left braces with the unicode character so we can swap it back later - let processedString = string.replace(/\\{/g, '\\U+007B'); + let processedString = string.replace(/\\{/g, "\\U+007B"); // Interpolate {key} patterns with data values processedString = processedString.replace(interpolate, (_match, key) => { const trimmed = key.trim(); @@ -29,5 +29,5 @@ module.exports = (string, data) => { return data[trimmed]; }); // Replace our swapped out left braces and any escaped right braces - return processedString.replace(/\\U\+007B/g, '{').replace(/\\}/g, '}'); + return processedString.replace(/\\U\+007B/g, "{").replace(/\\}/g, "}"); }; diff --git a/packages/tpl/package.json b/packages/tpl/package.json index 43cf38558..c38b1e692 100644 --- a/packages/tpl/package.json +++ b/packages/tpl/package.json @@ -1,14 +1,21 @@ { "name": "@tryghost/tpl", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/tpl" }, - "author": "Ghost Foundation", - "license": "MIT", + "files": [ + "index.js", + "lib" + ], "main": "index.js", + "publishConfig": { + "access": "public" + }, "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", @@ -16,15 +23,8 @@ "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, - "files": [ - "index.js", - "lib" - ], - "publishConfig": { - "access": "public" - }, + "dependencies": {}, "devDependencies": { "sinon": "21.0.3" - }, - "dependencies": {} + } } diff --git a/packages/tpl/test/.eslintrc.js b/packages/tpl/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/tpl/test/.eslintrc.js +++ b/packages/tpl/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/tpl/test/lodash.test.js b/packages/tpl/test/lodash.test.js index 7f4fa9808..344844941 100644 --- a/packages/tpl/test/lodash.test.js +++ b/packages/tpl/test/lodash.test.js @@ -1,9 +1,9 @@ -const assert = require('assert/strict'); +const assert = require("assert/strict"); -describe('Lodash Template', function () { - it('Does not get clobbered by this lib', function () { - require('../lib/tpl'); - let _ = require('lodash'); +describe("Lodash Template", function () { + it("Does not get clobbered by this lib", function () { + require("../lib/tpl"); + let _ = require("lodash"); // @ts-ignore assert.deepEqual(_.templateSettings.interpolate, /<%=([\s\S]+?)%>/g); diff --git a/packages/tpl/test/tpl.test.js b/packages/tpl/test/tpl.test.js index 8ac62d6d6..007516b3c 100644 --- a/packages/tpl/test/tpl.test.js +++ b/packages/tpl/test/tpl.test.js @@ -1,89 +1,89 @@ -const assert = require('assert/strict'); -const tpl = require('../'); +const assert = require("assert/strict"); +const tpl = require("../"); -describe('tpl', function () { - it('Can handle a plain string', function () { - const string = 'My template'; +describe("tpl", function () { + it("Can handle a plain string", function () { + const string = "My template"; const result = tpl(string); - assert.equal(result, 'My template'); + assert.equal(result, "My template"); }); - it('Can handle a string with data', function () { - const string = 'Go visit {url}'; - const data = {url: 'https://example.com'}; + it("Can handle a string with data", function () { + const string = "Go visit {url}"; + const data = { url: "https://example.com" }; let result = tpl(string, data); - assert.equal(result, 'Go visit https://example.com'); + assert.equal(result, "Go visit https://example.com"); }); - it('Can mix interpolation handlebars in the same message', function () { - const string = '{{#get}} helper took {totalMs}ms to complete'; + it("Can mix interpolation handlebars in the same message", function () { + const string = "{{#get}} helper took {totalMs}ms to complete"; const data = { - totalMs: '500' + totalMs: "500", }; let result = tpl(string, data); - assert.equal(result, '{{#get}} helper took 500ms to complete'); + assert.equal(result, "{{#get}} helper took 500ms to complete"); }); - it('Can mix interpolation with handlebars-block helpers without escaping', function () { - const string = '{{#{helperName}}} helper took {totalMs}ms to complete'; + it("Can mix interpolation with handlebars-block helpers without escaping", function () { + const string = "{{#{helperName}}} helper took {totalMs}ms to complete"; const data = { - helperName: 'get', - totalMs: '500' + helperName: "get", + totalMs: "500", }; let result = tpl(string, data); - assert.equal(result, '{{#get}} helper took 500ms to complete'); + assert.equal(result, "{{#get}} helper took 500ms to complete"); }); - it('ignores 3 braces', function () { - const string = 'The {{{helperName}}} helper is not available.'; + it("ignores 3 braces", function () { + const string = "The {{{helperName}}} helper is not available."; const data = { - helperName: 'get', - totalMs: '500' + helperName: "get", + totalMs: "500", }; let result = tpl(string, data); - assert.equal(result, 'The {{{helperName}}} helper is not available.'); + assert.equal(result, "The {{{helperName}}} helper is not available."); }); - it('has a simple bare minimum escaping needed', function () { - const string = 'The {\\{{helperName}}} helper is not available.'; + it("has a simple bare minimum escaping needed", function () { + const string = "The {\\{{helperName}}} helper is not available."; const data = { - helperName: 'get', - totalMs: '500' + helperName: "get", + totalMs: "500", }; let result = tpl(string, data); - assert.equal(result, 'The {{get}} helper is not available.'); + assert.equal(result, "The {{get}} helper is not available."); }); - it('Can handle escaped left braces', function () { - const string = 'The \\{\\{{helperName}}} helper is not available.'; + it("Can handle escaped left braces", function () { + const string = "The \\{\\{{helperName}}} helper is not available."; const data = { - helperName: 'get', - totalMs: '500' + helperName: "get", + totalMs: "500", }; let result = tpl(string, data); - assert.equal(result, 'The {{get}} helper is not available.'); + assert.equal(result, "The {{get}} helper is not available."); }); - it('Can handle escaped right braces as well', function () { - const string = 'The \\{\\{{helperName}\\}\\} helper is not available.'; + it("Can handle escaped right braces as well", function () { + const string = "The \\{\\{{helperName}\\}\\} helper is not available."; const data = { - helperName: 'get', - totalMs: '500' + helperName: "get", + totalMs: "500", }; let result = tpl(string, data); - assert.equal(result, 'The {{get}} helper is not available.'); + assert.equal(result, "The {{get}} helper is not available."); }); - it('Returns a sensible error if data is missing', function () { - const string = '{helperName} helper took {totalMs}ms to complete'; + it("Returns a sensible error if data is missing", function () { + const string = "{helperName} helper took {totalMs}ms to complete"; const data = { - totalMs: '500' + totalMs: "500", }; let resultFn = () => { diff --git a/packages/tsconfig.json b/packages/tsconfig.json index f115f241e..a34544f6d 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -6,7 +6,7 @@ /* Visit https://aka.ms/tsconfig to read more about this file */ /* Projects */ - "incremental": false, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + "incremental": false /* Save .tsbuildinfo files to allow for incremental compilation of projects. */, // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ @@ -14,8 +14,10 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - "lib": ["es2022"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "target": "es2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "lib": [ + "es2022" + ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ @@ -28,7 +30,7 @@ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ /* Modules */ - "module": "commonjs", /* Specify what module code is generated. */ + "module": "commonjs" /* Specify what module code is generated. */, // "rootDir": "./", /* Specify the root folder within your source files. */ // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ @@ -37,14 +39,14 @@ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ "types": [ "node" - ], /* Specify type package names to be included without being referenced in a source file. */ + ] /* Specify type package names to be included without being referenced in a source file. */, // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ - "resolveJsonModule": true, /* Enable importing .json files. */ + "resolveJsonModule": true /* Enable importing .json files. */, // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ @@ -54,10 +56,10 @@ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ /* Emit */ - "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, // "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + "sourceMap": true /* Create source map files for emitted JavaScript files. */, // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ // "outDir": "build", /* Specify an output folder for all emitted files. */ @@ -82,13 +84,13 @@ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + "strict": true /* Enable all strict type-checking options. */, + "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */, // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ @@ -109,6 +111,6 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ } } diff --git a/packages/validator/README.md b/packages/validator/README.md index f3d9f4970..e71913e3c 100644 --- a/packages/validator/README.md +++ b/packages/validator/README.md @@ -8,36 +8,30 @@ or `yarn add @tryghost/validator` - ## Purpose Ghost validation utilities built on validator.js, including custom validators and structured validation errors. ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests +# Copyright & License - - -# Copyright & License - -Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/validator/index.js b/packages/validator/index.js index af24c9c8d..e46bb61ed 100644 --- a/packages/validator/index.js +++ b/packages/validator/index.js @@ -1,2 +1,2 @@ -module.exports = require('./lib/validator'); -module.exports.validate = require('./lib/validate'); +module.exports = require("./lib/validator"); +module.exports.validate = require("./lib/validate"); diff --git a/packages/validator/lib/is-byte-length.js b/packages/validator/lib/is-byte-length.js index d0b229515..ec6d00a5f 100644 --- a/packages/validator/lib/is-byte-length.js +++ b/packages/validator/lib/is-byte-length.js @@ -1,17 +1,18 @@ -const assertString = require('./util/assert-string'); +const assertString = require("./util/assert-string"); /* eslint-disable prefer-rest-params */ module.exports = function isByteLength(str, options) { assertString(str); let min; let max; - if (typeof (options) === 'object') { + if (typeof options === "object") { min = options.min || 0; max = options.max; - } else { // backwards compatibility: isByteLength(str, min [, max]) + } else { + // backwards compatibility: isByteLength(str, min [, max]) min = arguments[1]; max = arguments[2]; } const len = encodeURI(str).split(/%..|./).length - 1; - return len >= min && (typeof max === 'undefined' || len <= max); + return len >= min && (typeof max === "undefined" || len <= max); }; diff --git a/packages/validator/lib/is-email.js b/packages/validator/lib/is-email.js index f81205433..a5408856b 100644 --- a/packages/validator/lib/is-email.js +++ b/packages/validator/lib/is-email.js @@ -3,21 +3,21 @@ * https://github.com/validatorjs/validator.js/blob/531dc7f1f75613bec75c6d888b46480455e78dc7/src/lib/isEmail.js */ /* eslint-disable camelcase */ -const assertString = require('./util/assert-string'); -const merge = require('./util/merge'); -const isByteLength = require('./is-byte-length'); -const isFQDN = require('./is-fqdn'); -const isIP = require('./is-ip'); +const assertString = require("./util/assert-string"); +const merge = require("./util/merge"); +const isByteLength = require("./is-byte-length"); +const isFQDN = require("./is-fqdn"); +const isIP = require("./is-ip"); const default_email_options = { allow_display_name: false, require_display_name: false, allow_utf8_local_part: true, require_tld: true, - blacklisted_chars: '', + blacklisted_chars: "", ignore_max_length: false, host_blacklist: [], - host_whitelist: [] + host_whitelist: [], }; /* eslint-disable max-len */ @@ -25,9 +25,11 @@ const default_email_options = { const splitNameAddress = /^([^\x00-\x1F\x7F-\x9F\cX]+)$)/g, ''); + str = str.replace(display_name, "").replace(/(^<|>$)/g, ""); // sometimes need to trim the last space to get the display name // because there may be a space between display name and email address // eg. myname // the display name is `myname` instead of `myname `, so need to trim the last space - if (display_name.endsWith(' ')) { + if (display_name.endsWith(" ")) { display_name = display_name.slice(0, -1); } @@ -95,7 +98,7 @@ module.exports = function isEmail(str, options) { return false; } - const parts = str.split('@'); + const parts = str.split("@"); const domain = parts.pop(); const lower_domain = domain.toLowerCase(); @@ -107,9 +110,12 @@ module.exports = function isEmail(str, options) { return false; } - let user = parts.join('@'); + let user = parts.join("@"); - if (options.domain_specific_validation && (lower_domain === 'gmail.com' || lower_domain === 'googlemail.com')) { + if ( + options.domain_specific_validation && + (lower_domain === "gmail.com" || lower_domain === "googlemail.com") + ) { /* Previously we removed dots for gmail addresses before validating. This was removed because it allows `multiple..dots@gmail.com` @@ -120,14 +126,14 @@ module.exports = function isEmail(str, options) { user = user.toLowerCase(); // Removing sub-address from username before gmail validation - const username = user.split('+')[0]; + const username = user.split("+")[0]; // Dots are not included in gmail length restriction - if (!isByteLength(username.replace(/\./g, ''), {min: 6, max: 30})) { + if (!isByteLength(username.replace(/\./g, ""), { min: 6, max: 30 })) { return false; } - const user_parts = username.split('.'); + const user_parts = username.split("."); for (let i = 0; i < user_parts.length; i++) { if (!gmailUserPart.test(user_parts[i])) { return false; @@ -135,20 +141,20 @@ module.exports = function isEmail(str, options) { } } - if (options.ignore_max_length === false && ( - !isByteLength(user, {max: 64}) || - !isByteLength(domain, {max: 254})) + if ( + options.ignore_max_length === false && + (!isByteLength(user, { max: 64 }) || !isByteLength(domain, { max: 254 })) ) { return false; } - if (!isFQDN(domain, {require_tld: options.require_tld})) { + if (!isFQDN(domain, { require_tld: options.require_tld })) { if (!options.allow_ip_domain) { return false; } if (!isIP(domain)) { - if (!domain.startsWith('[') || !domain.endsWith(']')) { + if (!domain.startsWith("[") || !domain.endsWith("]")) { return false; } @@ -162,22 +168,21 @@ module.exports = function isEmail(str, options) { if (user[0] === '"') { user = user.slice(1, user.length - 1); - return options.allow_utf8_local_part ? - quotedEmailUserUtf8.test(user) : - quotedEmailUser.test(user); + return options.allow_utf8_local_part + ? quotedEmailUserUtf8.test(user) + : quotedEmailUser.test(user); } - const pattern = options.allow_utf8_local_part ? - emailUserUtf8Part : emailUserPart; + const pattern = options.allow_utf8_local_part ? emailUserUtf8Part : emailUserPart; - const user_parts = user.split('.'); + const user_parts = user.split("."); for (let i = 0; i < user_parts.length; i++) { if (!pattern.test(user_parts[i])) { return false; } } if (options.blacklisted_chars) { - if (user.search(new RegExp(`[${options.blacklisted_chars}]+`, 'g')) !== -1) { + if (user.search(new RegExp(`[${options.blacklisted_chars}]+`, "g")) !== -1) { return false; } } diff --git a/packages/validator/lib/is-fqdn.js b/packages/validator/lib/is-fqdn.js index 322f00902..3dfc2d26a 100644 --- a/packages/validator/lib/is-fqdn.js +++ b/packages/validator/lib/is-fqdn.js @@ -3,15 +3,15 @@ * https://github.com/validatorjs/validator.js/blob/531dc7f1f75613bec75c6d888b46480455e78dc7/src/lib/isFQDN.js */ /* eslint-disable camelcase */ -const assertString = require('./util/assert-string'); -const merge = require('./util/merge'); +const assertString = require("./util/assert-string"); +const merge = require("./util/merge"); const default_fqdn_options = { require_tld: true, allow_underscores: false, allow_trailing_dot: false, allow_numeric_tld: false, - allow_wildcard: false + allow_wildcard: false, }; module.exports = function isFQDN(str, options) { @@ -19,25 +19,30 @@ module.exports = function isFQDN(str, options) { options = merge(options, default_fqdn_options); /* Remove the optional trailing dot before checking validity */ - if (options.allow_trailing_dot && str[str.length - 1] === '.') { + if (options.allow_trailing_dot && str[str.length - 1] === ".") { str = str.substring(0, str.length - 1); } /* Remove the optional wildcard before checking validity */ - if (options.allow_wildcard === true && str.indexOf('*.') === 0) { + if (options.allow_wildcard === true && str.indexOf("*.") === 0) { str = str.substring(2); } - const parts = str.split('.'); + const parts = str.split("."); const tld = parts[parts.length - 1]; if (options.require_tld) { - // disallow fqdns without tld + // disallow fqdns without tld if (parts.length < 2) { return false; } - if (!options.allow_numeric_tld && !/^([a-z\u00A1-\u00A8\u00AA-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}|xn[a-z0-9-]{2,})$/i.test(tld)) { + if ( + !options.allow_numeric_tld && + !/^([a-z\u00A1-\u00A8\u00AA-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}|xn[a-z0-9-]{2,})$/i.test( + tld, + ) + ) { return false; } diff --git a/packages/validator/lib/is-ip.js b/packages/validator/lib/is-ip.js index baa1e12a8..60cf889d2 100644 --- a/packages/validator/lib/is-ip.js +++ b/packages/validator/lib/is-ip.js @@ -3,7 +3,7 @@ * https://github.com/validatorjs/validator.js/blob/531dc7f1f75613bec75c6d888b46480455e78dc7/src/lib/isIP.js */ -const assertString = require('./util/assert-string'); +const assertString = require("./util/assert-string"); /** 11.3. Examples @@ -33,32 +33,34 @@ const assertString = require('./util/assert-string'); where the interface "ne0" belongs to the 1st link, "pvc1.3" belongs to the 5th link, and "interface10" belongs to the 10th organization. * * */ -const IPv4SegmentFormat = '(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])'; +const IPv4SegmentFormat = "(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])"; const IPv4AddressFormat = `(${IPv4SegmentFormat}[.]){3}${IPv4SegmentFormat}`; const IPv4AddressRegExp = new RegExp(`^${IPv4AddressFormat}$`); -const IPv6SegmentFormat = '(?:[0-9a-fA-F]{1,4})'; -const IPv6AddressRegExp = new RegExp('^(' + - `(?:${IPv6SegmentFormat}:){7}(?:${IPv6SegmentFormat}|:)|` + - `(?:${IPv6SegmentFormat}:){6}(?:${IPv4AddressFormat}|:${IPv6SegmentFormat}|:)|` + - `(?:${IPv6SegmentFormat}:){5}(?::${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,2}|:)|` + - `(?:${IPv6SegmentFormat}:){4}(?:(:${IPv6SegmentFormat}){0,1}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,3}|:)|` + - `(?:${IPv6SegmentFormat}:){3}(?:(:${IPv6SegmentFormat}){0,2}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,4}|:)|` + - `(?:${IPv6SegmentFormat}:){2}(?:(:${IPv6SegmentFormat}){0,3}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,5}|:)|` + - `(?:${IPv6SegmentFormat}:){1}(?:(:${IPv6SegmentFormat}){0,4}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,6}|:)|` + - `(?::((?::${IPv6SegmentFormat}){0,5}:${IPv4AddressFormat}|(?::${IPv6SegmentFormat}){1,7}|:))` + - ')(%[0-9a-zA-Z-.:]{1,})?$'); +const IPv6SegmentFormat = "(?:[0-9a-fA-F]{1,4})"; +const IPv6AddressRegExp = new RegExp( + "^(" + + `(?:${IPv6SegmentFormat}:){7}(?:${IPv6SegmentFormat}|:)|` + + `(?:${IPv6SegmentFormat}:){6}(?:${IPv4AddressFormat}|:${IPv6SegmentFormat}|:)|` + + `(?:${IPv6SegmentFormat}:){5}(?::${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,2}|:)|` + + `(?:${IPv6SegmentFormat}:){4}(?:(:${IPv6SegmentFormat}){0,1}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,3}|:)|` + + `(?:${IPv6SegmentFormat}:){3}(?:(:${IPv6SegmentFormat}){0,2}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,4}|:)|` + + `(?:${IPv6SegmentFormat}:){2}(?:(:${IPv6SegmentFormat}){0,3}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,5}|:)|` + + `(?:${IPv6SegmentFormat}:){1}(?:(:${IPv6SegmentFormat}){0,4}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,6}|:)|` + + `(?::((?::${IPv6SegmentFormat}){0,5}:${IPv4AddressFormat}|(?::${IPv6SegmentFormat}){1,7}|:))` + + ")(%[0-9a-zA-Z-.:]{1,})?$", +); -module.exports = function isIP(str, version = '') { +module.exports = function isIP(str, version = "") { assertString(str); version = String(version); if (!version) { return isIP(str, 4) || isIP(str, 6); } - if (version === '4') { + if (version === "4") { return IPv4AddressRegExp.test(str); } - if (version === '6') { + if (version === "6") { return IPv6AddressRegExp.test(str); } return false; diff --git a/packages/validator/lib/util/assert-string.js b/packages/validator/lib/util/assert-string.js index 4658fbbb0..4419f5fd2 100644 --- a/packages/validator/lib/util/assert-string.js +++ b/packages/validator/lib/util/assert-string.js @@ -1,22 +1,22 @@ -const errors = require('@tryghost/errors'); +const errors = require("@tryghost/errors"); /** * This file is a copy of validator.js assertString util - v13.7.0: * https://github.com/validatorjs/validator.js/blob/531dc7f1f75613bec75c6d888b46480455e78dc7/src/lib/util/assertString.js */ module.exports = function assertString(input) { - const isString = typeof input === 'string' || input instanceof String; + const isString = typeof input === "string" || input instanceof String; if (!isString) { let invalidType = typeof input; if (input === null) { - invalidType = 'null'; - } else if (invalidType === 'object') { + invalidType = "null"; + } else if (invalidType === "object") { invalidType = input.constructor.name; } throw new errors.ValidationError({ - message: `Expected a string but received a ${invalidType}` + message: `Expected a string but received a ${invalidType}`, }); } }; diff --git a/packages/validator/lib/util/merge.js b/packages/validator/lib/util/merge.js index 6beaffb43..f8952426e 100644 --- a/packages/validator/lib/util/merge.js +++ b/packages/validator/lib/util/merge.js @@ -5,7 +5,7 @@ module.exports = function merge(obj = {}, defaults) { for (const key in defaults) { - if (typeof obj[key] === 'undefined') { + if (typeof obj[key] === "undefined") { obj[key] = defaults[key]; } } diff --git a/packages/validator/lib/validate.js b/packages/validator/lib/validate.js index 69497cfa9..d3c2c9c7e 100644 --- a/packages/validator/lib/validate.js +++ b/packages/validator/lib/validate.js @@ -1,14 +1,14 @@ -const _ = require('lodash'); -const validator = require('./validator'); +const _ = require("lodash"); +const validator = require("./validator"); -const tpl = require('@tryghost/tpl'); -const errors = require('@tryghost/errors'); +const tpl = require("@tryghost/tpl"); +const errors = require("@tryghost/errors"); const messages = { - validationFailed: 'Validation ({validationName}) failed for {key}', + validationFailed: "Validation ({validationName}) failed for {key}", validationFailedTypes: { - isLength: 'Value in [{tableName}.{key}] exceeds maximum length of {max} characters.' - } + isLength: "Value in [{tableName}.{key}] exceeds maximum length of {max} characters.", + }, }; /** @@ -53,22 +53,30 @@ function validate(value, key, validations, tableName) { if (validator[validationName].apply(validator, validationOptions) !== goodResult) { // CASE: You can define specific messages for validators e.g. isLength if (_.has(messages.validationFailedTypes, validationName)) { - message = tpl(messages.validationFailedTypes[validationName], _.merge({ - validationName: validationName, - key: key, - tableName: tableName - }, validationOptions[1])); + message = tpl( + messages.validationFailedTypes[validationName], + _.merge( + { + validationName: validationName, + key: key, + tableName: tableName, + }, + validationOptions[1], + ), + ); } else { message = tpl(messages.validationFailed, { validationName: validationName, - key: key + key: key, }); } - validationErrors.push(new errors.ValidationError({ - message: message, - context: `${tableName}.${key}` - })); + validationErrors.push( + new errors.ValidationError({ + message: message, + context: `${tableName}.${key}`, + }), + ); } validationOptions.shift(); diff --git a/packages/validator/lib/validator.js b/packages/validator/lib/validator.js index 3a8e2b199..865b000b5 100644 --- a/packages/validator/lib/validator.js +++ b/packages/validator/lib/validator.js @@ -1,27 +1,27 @@ -const _ = require('lodash'); +const _ = require("lodash"); -const baseValidator = require('validator'); -const moment = require('moment-timezone'); -const assert = require('assert'); +const baseValidator = require("validator"); +const moment = require("moment-timezone"); +const assert = require("assert"); -const isEmailCustom = require('./is-email'); +const isEmailCustom = require("./is-email"); const allowedValidators = [ - 'isLength', - 'isEmpty', - 'isURL', - 'isEmail', - 'isIn', - 'isUUID', - 'isBoolean', - 'isInt', - 'isLowercase', - 'equals', - 'matches' + "isLength", + "isEmpty", + "isURL", + "isEmail", + "isIn", + "isUUID", + "isBoolean", + "isInt", + "isLowercase", + "equals", + "matches", ]; function assertString(input) { - assert(typeof input === 'string', 'Validator validates strings only'); + assert(typeof input === "string", "Validator validates strings only"); } const validators = {}; @@ -39,7 +39,7 @@ validators.isTimezone = function isTimezone(str) { validators.isEmptyOrURL = function isEmptyOrURL(str) { assertString(str); - return (validators.isEmpty(str) || validators.isURL(str, {require_protocol: false})); + return validators.isEmpty(str) || validators.isURL(str, { require_protocol: false }); }; validators.isSlug = function isSlug(str) { @@ -47,7 +47,7 @@ validators.isSlug = function isSlug(str) { return validators.matches(str, /^[a-z0-9\-_]+$/); }; -validators.isEmail = function isEmail(str, options = {legacy: true}) { +validators.isEmail = function isEmail(str, options = { legacy: true }) { assertString(str); // Use the latest email validator if legacy is set to false if (!options?.legacy) { diff --git a/packages/validator/package.json b/packages/validator/package.json index 51be92332..49c25fce3 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -1,14 +1,21 @@ { "name": "@tryghost/validator", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/validator" }, - "author": "Ghost Foundation", - "license": "MIT", + "files": [ + "index.js", + "lib" + ], "main": "index.js", + "publishConfig": { + "access": "public" + }, "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", @@ -16,13 +23,6 @@ "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, - "files": [ - "index.js", - "lib" - ], - "publishConfig": { - "access": "public" - }, "dependencies": { "@tryghost/errors": "^3.0.3", "@tryghost/tpl": "^2.0.3", diff --git a/packages/validator/test/.eslintrc.js b/packages/validator/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/validator/test/.eslintrc.js +++ b/packages/validator/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/validator/test/internals.test.js b/packages/validator/test/internals.test.js index 6ff008334..d2b1d256f 100644 --- a/packages/validator/test/internals.test.js +++ b/packages/validator/test/internals.test.js @@ -1,145 +1,157 @@ -const assert = require('assert/strict'); - -const validate = require('../lib/validate'); -const validator = require('../lib/validator'); -const isEmail = require('../lib/is-email'); -const isFQDN = require('../lib/is-fqdn'); -const isIP = require('../lib/is-ip'); -const isByteLength = require('../lib/is-byte-length'); -const assertString = require('../lib/util/assert-string'); - -describe('Validator internals', function () { - describe('validate()', function () { - it('returns no errors for valid values', function () { - const errors = validate('abc', 'name', {isLength: {max: 10}}, 'users'); +const assert = require("assert/strict"); + +const validate = require("../lib/validate"); +const validator = require("../lib/validator"); +const isEmail = require("../lib/is-email"); +const isFQDN = require("../lib/is-fqdn"); +const isIP = require("../lib/is-ip"); +const isByteLength = require("../lib/is-byte-length"); +const assertString = require("../lib/util/assert-string"); + +describe("Validator internals", function () { + describe("validate()", function () { + it("returns no errors for valid values", function () { + const errors = validate("abc", "name", { isLength: { max: 10 } }, "users"); assert.deepEqual(errors, []); }); - it('supports boolean validation options', function () { - const errors = validate('ABC', 'name', {isLowercase: false}, 'users'); + it("supports boolean validation options", function () { + const errors = validate("ABC", "name", { isLowercase: false }, "users"); assert.deepEqual(errors, []); }); - it('returns type-specific message for isLength', function () { - const errors = validate('abc', 'name', {isLength: {max: 2}}, 'users'); + it("returns type-specific message for isLength", function () { + const errors = validate("abc", "name", { isLength: { max: 2 } }, "users"); assert.equal(errors.length, 1); - assert.equal(errors[0].message, 'Value in [users.name] exceeds maximum length of 2 characters.'); - assert.equal(errors[0].context, 'users.name'); + assert.equal( + errors[0].message, + "Value in [users.name] exceeds maximum length of 2 characters.", + ); + assert.equal(errors[0].context, "users.name"); }); - it('returns default validation message for other validators', function () { - const errors = validate('ABC', 'name', {isLowercase: true}, 'users'); + it("returns default validation message for other validators", function () { + const errors = validate("ABC", "name", { isLowercase: true }, "users"); assert.equal(errors.length, 1); - assert.equal(errors[0].message, 'Validation (isLowercase) failed for name'); - assert.equal(errors[0].context, 'users.name'); + assert.equal(errors[0].message, "Validation (isLowercase) failed for name"); + assert.equal(errors[0].context, "users.name"); }); }); - describe('lib/validator custom methods', function () { - it('isTimezone validates real timezones', function () { - assert.equal(validator.isTimezone('Europe/London'), true); - assert.equal(validator.isTimezone('Not/AZone'), false); + describe("lib/validator custom methods", function () { + it("isTimezone validates real timezones", function () { + assert.equal(validator.isTimezone("Europe/London"), true); + assert.equal(validator.isTimezone("Not/AZone"), false); }); - it('isSlug validates slug format', function () { - assert.equal(validator.isSlug('a-valid_slug-1'), true); - assert.equal(validator.isSlug('not valid slug'), false); + it("isSlug validates slug format", function () { + assert.equal(validator.isSlug("a-valid_slug-1"), true); + assert.equal(validator.isSlug("not valid slug"), false); }); - it('custom validators enforce string input', function () { + it("custom validators enforce string input", function () { assert.throws(() => validator.isTimezone(1), /Validator validates strings only/); assert.throws(() => validator.isSlug({}), /Validator validates strings only/); assert.throws(() => validator.isEmptyOrURL(null), /Validator validates strings only/); }); }); - describe('is-email', function () { - it('supports display name options', function () { - assert.equal(isEmail('Name ', {allow_display_name: true}), true); - assert.equal(isEmail('member@example.com', {require_display_name: true}), false); - assert.equal(isEmail('" " ', {allow_display_name: true}), false); - assert.equal(isEmail('Bad;Name ', {allow_display_name: true}), false); - assert.equal(isEmail('"Bad"Name" ', {allow_display_name: true}), false); - }); - - it('supports host allow and deny lists', function () { - assert.equal(isEmail('member@example.com', {host_blacklist: ['example.com']}), false); - assert.equal(isEmail('member@test.com', {host_whitelist: ['example.com']}), false); - assert.equal(isEmail('member@example.com', {host_whitelist: ['example.com']}), true); - }); - - it('supports domain specific validation and max length options', function () { - assert.equal(isEmail('abc123@gmail.com', {domain_specific_validation: true}), true); - assert.equal(isEmail('abc123@googlemail.com', {domain_specific_validation: true}), true); - assert.equal(isEmail('a@gmail.com', {domain_specific_validation: true}), false); - assert.equal(isEmail('abc_123@gmail.com', {domain_specific_validation: true}), false); - assert.equal(isEmail(`${'a'.repeat(250)}@x.com`, {ignore_max_length: true}), true); - assert.equal(isEmail(`${'a'.repeat(250)}@x.com`, {ignore_max_length: false}), false); - assert.equal(isEmail(`${'a'.repeat(65)}@x.com`, {ignore_max_length: false}), false); - }); - - it('supports ip-domain and local-part options', function () { - assert.equal(isEmail('member@127.0.0.1'), false); - assert.equal(isEmail('member@127.0.0.1', {allow_ip_domain: true}), true); - assert.equal(isEmail('member@[127.0.0.1]', {allow_ip_domain: true}), true); - assert.equal(isEmail('member@[]', {allow_ip_domain: true}), false); - assert.equal(isEmail('member@invalid_domain', {allow_ip_domain: true}), false); - assert.equal(isEmail('"member"@example.com', {allow_utf8_local_part: false}), true); - assert.equal(isEmail('"mémber"@example.com', {allow_utf8_local_part: true}), true); - assert.equal(isEmail('mémber@example.com', {allow_utf8_local_part: false}), false); - assert.equal(isEmail('member()@example.com'), false); - assert.equal(isEmail('member@example.com', {blacklisted_chars: 'm'}), false); - assert.equal(isEmail('member@example.com', {blacklisted_chars: 'z'}), true); + describe("is-email", function () { + it("supports display name options", function () { + assert.equal(isEmail("Name ", { allow_display_name: true }), true); + assert.equal(isEmail("member@example.com", { require_display_name: true }), false); + assert.equal(isEmail('" " ', { allow_display_name: true }), false); + assert.equal( + isEmail("Bad;Name ", { allow_display_name: true }), + false, + ); + assert.equal( + isEmail('"Bad"Name" ', { allow_display_name: true }), + false, + ); + }); + + it("supports host allow and deny lists", function () { + assert.equal(isEmail("member@example.com", { host_blacklist: ["example.com"] }), false); + assert.equal(isEmail("member@test.com", { host_whitelist: ["example.com"] }), false); + assert.equal(isEmail("member@example.com", { host_whitelist: ["example.com"] }), true); + }); + + it("supports domain specific validation and max length options", function () { + assert.equal(isEmail("abc123@gmail.com", { domain_specific_validation: true }), true); + assert.equal( + isEmail("abc123@googlemail.com", { domain_specific_validation: true }), + true, + ); + assert.equal(isEmail("a@gmail.com", { domain_specific_validation: true }), false); + assert.equal(isEmail("abc_123@gmail.com", { domain_specific_validation: true }), false); + assert.equal(isEmail(`${"a".repeat(250)}@x.com`, { ignore_max_length: true }), true); + assert.equal(isEmail(`${"a".repeat(250)}@x.com`, { ignore_max_length: false }), false); + assert.equal(isEmail(`${"a".repeat(65)}@x.com`, { ignore_max_length: false }), false); + }); + + it("supports ip-domain and local-part options", function () { + assert.equal(isEmail("member@127.0.0.1"), false); + assert.equal(isEmail("member@127.0.0.1", { allow_ip_domain: true }), true); + assert.equal(isEmail("member@[127.0.0.1]", { allow_ip_domain: true }), true); + assert.equal(isEmail("member@[]", { allow_ip_domain: true }), false); + assert.equal(isEmail("member@invalid_domain", { allow_ip_domain: true }), false); + assert.equal(isEmail('"member"@example.com', { allow_utf8_local_part: false }), true); + assert.equal(isEmail('"mémber"@example.com', { allow_utf8_local_part: true }), true); + assert.equal(isEmail("mémber@example.com", { allow_utf8_local_part: false }), false); + assert.equal(isEmail("member()@example.com"), false); + assert.equal(isEmail("member@example.com", { blacklisted_chars: "m" }), false); + assert.equal(isEmail("member@example.com", { blacklisted_chars: "z" }), true); }); }); - describe('is-fqdn', function () { - it('covers option branches', function () { - assert.equal(isFQDN('example.com'), true); - assert.equal(isFQDN('example'), false); - assert.equal(isFQDN('example.com.', {allow_trailing_dot: true}), true); - assert.equal(isFQDN('*.example.com'), false); - assert.equal(isFQDN('*.example.com', {allow_wildcard: true}), true); - assert.equal(isFQDN('my_domain.com'), false); - assert.equal(isFQDN('my_domain.com', {allow_underscores: true}), true); - assert.equal(isFQDN('example.123'), false); - assert.equal(isFQDN('example.123', {allow_numeric_tld: true}), true); - assert.equal(isFQDN('exa mple.com'), false); - assert.equal(isFQDN('example.c om'), false); - assert.equal(isFQDN('example.c om', {allow_numeric_tld: true}), false); - assert.equal(isFQDN('example.123', {require_tld: false}), false); - assert.equal(isFQDN(`${'a'.repeat(64)}.com`), false); - assert.equal(isFQDN('example.com'), false); - assert.equal(isFQDN('-example.com'), false); + describe("is-fqdn", function () { + it("covers option branches", function () { + assert.equal(isFQDN("example.com"), true); + assert.equal(isFQDN("example"), false); + assert.equal(isFQDN("example.com.", { allow_trailing_dot: true }), true); + assert.equal(isFQDN("*.example.com"), false); + assert.equal(isFQDN("*.example.com", { allow_wildcard: true }), true); + assert.equal(isFQDN("my_domain.com"), false); + assert.equal(isFQDN("my_domain.com", { allow_underscores: true }), true); + assert.equal(isFQDN("example.123"), false); + assert.equal(isFQDN("example.123", { allow_numeric_tld: true }), true); + assert.equal(isFQDN("exa mple.com"), false); + assert.equal(isFQDN("example.c om"), false); + assert.equal(isFQDN("example.c om", { allow_numeric_tld: true }), false); + assert.equal(isFQDN("example.123", { require_tld: false }), false); + assert.equal(isFQDN(`${"a".repeat(64)}.com`), false); + assert.equal(isFQDN("example.com"), false); + assert.equal(isFQDN("-example.com"), false); }); }); - describe('is-ip', function () { - it('supports v4, v6, and explicit version checks', function () { - assert.equal(isIP('127.0.0.1'), true); - assert.equal(isIP('127.0.0.1', 4), true); - assert.equal(isIP('fe80::1234%1'), true); - assert.equal(isIP('fe80::1234%1', 6), true); - assert.equal(isIP('127.0.0.1', 5), false); + describe("is-ip", function () { + it("supports v4, v6, and explicit version checks", function () { + assert.equal(isIP("127.0.0.1"), true); + assert.equal(isIP("127.0.0.1", 4), true); + assert.equal(isIP("fe80::1234%1"), true); + assert.equal(isIP("fe80::1234%1", 6), true); + assert.equal(isIP("127.0.0.1", 5), false); }); }); - describe('is-byte-length', function () { - it('supports object options and legacy signature', function () { - assert.equal(isByteLength('abc', {min: 2, max: 3}), true); - assert.equal(isByteLength('abc', {min: 4, max: 5}), false); - assert.equal(isByteLength('abc', 2, 3), true); - assert.equal(isByteLength('abc', 4, 5), false); + describe("is-byte-length", function () { + it("supports object options and legacy signature", function () { + assert.equal(isByteLength("abc", { min: 2, max: 3 }), true); + assert.equal(isByteLength("abc", { min: 4, max: 5 }), false); + assert.equal(isByteLength("abc", 2, 3), true); + assert.equal(isByteLength("abc", 4, 5), false); }); }); - describe('assert-string', function () { - it('accepts string values', function () { - assert.doesNotThrow(() => assertString('hello')); - assert.doesNotThrow(() => assertString(new String('hello'))); + describe("assert-string", function () { + it("accepts string values", function () { + assert.doesNotThrow(() => assertString("hello")); + assert.doesNotThrow(() => assertString(new String("hello"))); }); - it('throws typed validation errors for invalid values', function () { + it("throws typed validation errors for invalid values", function () { assert.throws(() => assertString(null), /Expected a string but received a null/); assert.throws(() => assertString({}), /Expected a string but received a Object/); assert.throws(() => assertString(1), /Expected a string but received a number/); diff --git a/packages/validator/test/validate.test.js b/packages/validator/test/validate.test.js index 86b41b6e4..d9af04447 100644 --- a/packages/validator/test/validate.test.js +++ b/packages/validator/test/validate.test.js @@ -1,12 +1,12 @@ -const assert = require('assert/strict'); +const assert = require("assert/strict"); -const validator = require('../'); +const validator = require("../"); // Validate our customizations -describe('Validate', function () { - it('should export our required functions', function () { +describe("Validate", function () { + it("should export our required functions", function () { assert.ok(validator); - assert.ok(Object.prototype.hasOwnProperty.call(validator, 'validate')); - assert.equal(typeof validator.validate, 'function'); + assert.ok(Object.prototype.hasOwnProperty.call(validator, "validate")); + assert.equal(typeof validator.validate, "function"); }); }); diff --git a/packages/validator/test/validator.test.js b/packages/validator/test/validator.test.js index 4fcc62acd..a58c13bdc 100644 --- a/packages/validator/test/validator.test.js +++ b/packages/validator/test/validator.test.js @@ -1,55 +1,62 @@ -const assert = require('assert/strict'); - -const validator = require('../'); - -const validators = ['isLength', - 'isEmpty', - 'isURL', - 'isEmail', - 'isIn', - 'isUUID', - 'isBoolean', - 'isInt', - 'isLowercase', - 'equals', - 'matches' +const assert = require("assert/strict"); + +const validator = require("../"); + +const validators = [ + "isLength", + "isEmpty", + "isURL", + "isEmail", + "isIn", + "isUUID", + "isBoolean", + "isInt", + "isLowercase", + "equals", + "matches", ]; -const custom = ['isTimezone', 'isEmptyOrURL', 'isSlug']; +const custom = ["isTimezone", "isEmptyOrURL", "isSlug"]; -describe('Validator', function () { - it('should export our required functions', function () { +describe("Validator", function () { + it("should export our required functions", function () { assert.ok(validator); - const allMethods = validators.concat(custom).concat('validate'); + const allMethods = validators.concat(custom).concat("validate"); assert.deepEqual(Object.keys(validator), allMethods); }); - describe('Custom Validators', function () { - it('isEmptyOrUrl filters javascript urls', function () { - assert.equal(validator.isEmptyOrURL('javascript:alert(0)'), false); - assert.equal(validator.isEmptyOrURL('http://example.com/lol//'), false); - assert.equal(validator.isEmptyOrURL('http://example.com/lol?somequery='), false); - assert.equal(validator.isEmptyOrURL(''), true); - assert.equal(validator.isEmptyOrURL('http://localhost:2368'), true); - assert.equal(validator.isEmptyOrURL('http://example.com/test/'), true); - assert.equal(validator.isEmptyOrURL('http://www.example.com/test/'), true); - assert.equal(validator.isEmptyOrURL('http://example.com/foo?somequery=bar'), true); - assert.equal(validator.isEmptyOrURL('example.com/test/'), true); + describe("Custom Validators", function () { + it("isEmptyOrUrl filters javascript urls", function () { + assert.equal(validator.isEmptyOrURL("javascript:alert(0)"), false); + assert.equal( + validator.isEmptyOrURL("http://example.com/lol//"), + false, + ); + assert.equal( + validator.isEmptyOrURL("http://example.com/lol?somequery="), + false, + ); + assert.equal(validator.isEmptyOrURL(""), true); + assert.equal(validator.isEmptyOrURL("http://localhost:2368"), true); + assert.equal(validator.isEmptyOrURL("http://example.com/test/"), true); + assert.equal(validator.isEmptyOrURL("http://www.example.com/test/"), true); + assert.equal(validator.isEmptyOrURL("http://example.com/foo?somequery=bar"), true); + assert.equal(validator.isEmptyOrURL("example.com/test/"), true); }); - it('custom isEmail validator detects incorrect emails', function () { - assert.equal(validator.isEmail('member@example.com'), true); - assert.equal(validator.isEmail('member@example.com', {legacy: false}), true); + it("custom isEmail validator detects incorrect emails", function () { + assert.equal(validator.isEmail("member@example.com"), true); + assert.equal(validator.isEmail("member@example.com", { legacy: false }), true); - assert.equal(validator.isEmail('member@example'), false); - assert.equal(validator.isEmail('member@example', {legacy: false}), false); + assert.equal(validator.isEmail("member@example"), false); + assert.equal(validator.isEmail("member@example", { legacy: false }), false); // old email validator doesn't detect this as invalid - assert.equal(validator.isEmail('member@example.com�'), true); + assert.equal(validator.isEmail("member@example.com�"), true); // new email validator detects this as invalid - assert.equal(validator.isEmail('member@example.com�', {legacy: false}), false); + assert.equal(validator.isEmail("member@example.com�", { legacy: false }), false); }); }); }); diff --git a/packages/version/README.md b/packages/version/README.md index e8ceda962..639bf1b1c 100644 --- a/packages/version/README.md +++ b/packages/version/README.md @@ -8,36 +8,30 @@ or `yarn add @tryghost/version` - ## Purpose Version helpers that expose normalized Ghost version variants (`safe`, `full`, `original`). ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests - - - -# Copyright & License +# Copyright & License Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/version/index.js b/packages/version/index.js index f3277ba80..94ca0af43 100644 --- a/packages/version/index.js +++ b/packages/version/index.js @@ -1 +1 @@ -module.exports = require('./lib/version'); +module.exports = require("./lib/version"); diff --git a/packages/version/lib/version.js b/packages/version/lib/version.js index 8ab2e7a8e..941c8d199 100644 --- a/packages/version/lib/version.js +++ b/packages/version/lib/version.js @@ -1,11 +1,13 @@ -const path = require('path'); -const semver = require('semver'); -const rootUtils = require('@tryghost/root-utils'); -const packageInfo = require(path.join(rootUtils.getProcessRoot(), 'package.json')); +const path = require("path"); +const semver = require("semver"); +const rootUtils = require("@tryghost/root-utils"); +const packageInfo = require(path.join(rootUtils.getProcessRoot(), "package.json")); const version = packageInfo.version; const plainVersion = version.match(/^(\d+\.)?(\d+\.)?(\d+)/)[0]; const prereleaseParts = semver.prerelease(version); -const prereleaseVersion = prereleaseParts ? `${plainVersion}-${prereleaseParts.join('.')}` : plainVersion; +const prereleaseVersion = prereleaseParts + ? `${plainVersion}-${prereleaseParts.join(".")}` + : plainVersion; // major.minor module.exports.safe = version.match(/^(\d+\.)?(\d+)/)[0]; diff --git a/packages/version/package.json b/packages/version/package.json index 9c0d7dbc6..2c9495481 100644 --- a/packages/version/package.json +++ b/packages/version/package.json @@ -1,33 +1,33 @@ { "name": "@tryghost/version", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/version" }, - "author": "Ghost Foundation", - "license": "MIT", - "main": "index.js", - "scripts": { - "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" - }, "files": [ "index.js", "lib" ], + "main": "index.js", "publishConfig": { "access": "public" }, - "devDependencies": { - "sinon": "21.0.3" + "scripts": { + "dev": "echo \"Implement me!\"", + "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "dependencies": { "@tryghost/root-utils": "^2.0.3", "semver": "7.7.4" + }, + "devDependencies": { + "sinon": "21.0.3" } } diff --git a/packages/version/test/.eslintrc.js b/packages/version/test/.eslintrc.js index 829b601eb..6282aa6f4 100644 --- a/packages/version/test/.eslintrc.js +++ b/packages/version/test/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] + plugins: ["ghost"], + extends: ["plugin:ghost/test"], }; diff --git a/packages/version/test/index.test.js b/packages/version/test/index.test.js index c23822e0e..d58026820 100644 --- a/packages/version/test/index.test.js +++ b/packages/version/test/index.test.js @@ -1,9 +1,9 @@ -const assert = require('assert/strict'); +const assert = require("assert/strict"); -describe('Version index', function () { - it('exports version implementation', function () { - const indexExport = require('../index'); - const libExport = require('../lib/version'); +describe("Version index", function () { + it("exports version implementation", function () { + const indexExport = require("../index"); + const libExport = require("../lib/version"); assert.equal(indexExport, libExport); }); diff --git a/packages/version/test/version.test.js b/packages/version/test/version.test.js index ecefb1dbb..c8b982c6c 100644 --- a/packages/version/test/version.test.js +++ b/packages/version/test/version.test.js @@ -1,78 +1,78 @@ -const assert = require('assert/strict'); -const fs = require('fs'); -const os = require('os'); -const path = require('path'); +const assert = require("assert/strict"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); -const modulePath = require.resolve('../lib/version'); +const modulePath = require.resolve("../lib/version"); const loadVersionModuleFor = function loadVersionModuleFor(version) { const previousCwd = process.cwd(); - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'version-test-')); + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "version-test-")); try { - fs.writeFileSync(path.join(tempDir, 'package.json'), JSON.stringify({version})); + fs.writeFileSync(path.join(tempDir, "package.json"), JSON.stringify({ version })); process.chdir(tempDir); delete require.cache[modulePath]; - return require('../lib/version'); + return require("../lib/version"); } finally { process.chdir(previousCwd); - fs.rmSync(tempDir, {recursive: true, force: true}); + fs.rmSync(tempDir, { recursive: true, force: true }); delete require.cache[modulePath]; } }; -describe('Version', function () { - it('default', function () { - const version = '1.10.0'; +describe("Version", function () { + it("default", function () { + const version = "1.10.0"; const ghostVersionUtils = loadVersionModuleFor(version); assert.equal(ghostVersionUtils.full, version); assert.equal(ghostVersionUtils.original, version); - assert.equal(ghostVersionUtils.safe, '1.10'); + assert.equal(ghostVersionUtils.safe, "1.10"); }); - it('pre-release', function () { - const version = '1.11.1-beta'; + it("pre-release", function () { + const version = "1.11.1-beta"; const ghostVersionUtils = loadVersionModuleFor(version); assert.equal(ghostVersionUtils.full, version); assert.equal(ghostVersionUtils.original, version); - assert.equal(ghostVersionUtils.safe, '1.11'); + assert.equal(ghostVersionUtils.safe, "1.11"); }); - it('pre-release .1', function () { - const version = '1.11.1-alpha.1'; + it("pre-release .1", function () { + const version = "1.11.1-alpha.1"; const ghostVersionUtils = loadVersionModuleFor(version); assert.equal(ghostVersionUtils.full, version); assert.equal(ghostVersionUtils.original, version); - assert.equal(ghostVersionUtils.safe, '1.11'); + assert.equal(ghostVersionUtils.safe, "1.11"); }); - it('build', function () { - const version = '1.11.1+build'; + it("build", function () { + const version = "1.11.1+build"; const ghostVersionUtils = loadVersionModuleFor(version); - assert.equal(ghostVersionUtils.full, '1.11.1'); + assert.equal(ghostVersionUtils.full, "1.11.1"); assert.equal(ghostVersionUtils.original, version); - assert.equal(ghostVersionUtils.safe, '1.11'); + assert.equal(ghostVersionUtils.safe, "1.11"); }); - it('mixed', function () { - const version = '1.11.1-pre+build.1'; + it("mixed", function () { + const version = "1.11.1-pre+build.1"; const ghostVersionUtils = loadVersionModuleFor(version); - assert.equal(ghostVersionUtils.full, '1.11.1-pre'); + assert.equal(ghostVersionUtils.full, "1.11.1-pre"); assert.equal(ghostVersionUtils.original, version); - assert.equal(ghostVersionUtils.safe, '1.11'); + assert.equal(ghostVersionUtils.safe, "1.11"); }); - it('mixed 1', function () { - const version = '1.11.1-beta.12+build.2'; + it("mixed 1", function () { + const version = "1.11.1-beta.12+build.2"; const ghostVersionUtils = loadVersionModuleFor(version); - assert.equal(ghostVersionUtils.full, '1.11.1-beta.12'); + assert.equal(ghostVersionUtils.full, "1.11.1-beta.12"); assert.equal(ghostVersionUtils.original, version); - assert.equal(ghostVersionUtils.safe, '1.11'); + assert.equal(ghostVersionUtils.safe, "1.11"); }); }); diff --git a/packages/webhook-mock-receiver/README.md b/packages/webhook-mock-receiver/README.md index b6d5b0b3d..7e2f88bce 100644 --- a/packages/webhook-mock-receiver/README.md +++ b/packages/webhook-mock-receiver/README.md @@ -10,36 +10,30 @@ or `yarn add @tryghost/webhook-mock-receiver` - ## Purpose Test utility for mocking webhook endpoints and asserting captured payload/header snapshots. ## Usage - ## Develop This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests - - - -# Copyright & License +# Copyright & License Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/webhook-mock-receiver/index.js b/packages/webhook-mock-receiver/index.js index 59bbc87fe..a57a21d6a 100644 --- a/packages/webhook-mock-receiver/index.js +++ b/packages/webhook-mock-receiver/index.js @@ -1 +1 @@ -module.exports = require('./lib/WebhookMockReceiver'); +module.exports = require("./lib/WebhookMockReceiver"); diff --git a/packages/webhook-mock-receiver/lib/WebhookMockReceiver.js b/packages/webhook-mock-receiver/lib/WebhookMockReceiver.js index 9e2e6d8e1..ce9663a30 100644 --- a/packages/webhook-mock-receiver/lib/WebhookMockReceiver.js +++ b/packages/webhook-mock-receiver/lib/WebhookMockReceiver.js @@ -1,8 +1,8 @@ -const {AssertionError} = require('assert'); -const {URL} = require('url'); -const nock = require('nock'); +const { AssertionError } = require("assert"); +const { URL } = require("url"); +const nock = require("nock"); class WebhookMockReceiver { - constructor({snapshotManager}) { + constructor({ snapshotManager }) { this.body; this.headers; this._receiver; @@ -11,13 +11,13 @@ class WebhookMockReceiver { } recordRequest(body, options) { - this.body = {body}; - this.headers = {headers: options.headers}; + this.body = { body }; + this.headers = { headers: options.headers }; } async receivedRequest() { // @NOTE: figure out a better waiting mechanism here, don't allow it to hang forever - const {default: pWaitFor} = await import('p-wait-for'); + const { default: pWaitFor } = await import("p-wait-for"); await pWaitFor(() => this._receiver.isDone()); } @@ -36,7 +36,7 @@ class WebhookMockReceiver { // let the nock continue with the response return true; }) - .reply(200, {status: 'OK'}); + .reply(200, { status: "OK" }); return this; } @@ -52,9 +52,9 @@ class WebhookMockReceiver { const error = new AssertionError({}); let assertion = { properties: properties, - field: 'body', - type: 'body', - error + field: "body", + type: "body", + error, }; this.snapshotManager.assertSnapshot(this.body, assertion); @@ -66,9 +66,9 @@ class WebhookMockReceiver { const error = new AssertionError({}); let assertion = { properties: properties, - field: 'headers', - type: 'header', - error + field: "headers", + type: "header", + error, }; this.snapshotManager.assertSnapshot(this.headers, assertion); diff --git a/packages/webhook-mock-receiver/package.json b/packages/webhook-mock-receiver/package.json index d8416e04c..4bd3235d0 100644 --- a/packages/webhook-mock-receiver/package.json +++ b/packages/webhook-mock-receiver/package.json @@ -1,14 +1,21 @@ { "name": "@tryghost/webhook-mock-receiver", "version": "2.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/webhook-mock-receiver" }, - "author": "Ghost Foundation", - "license": "MIT", + "files": [ + "index.js", + "lib" + ], "main": "index.js", + "publishConfig": { + "access": "public" + }, "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", @@ -18,18 +25,11 @@ "posttest": "yarn lint", "lint:eslint": "yarn lint:code && yarn lint:test" }, - "files": [ - "index.js", - "lib" - ], - "publishConfig": { - "access": "public" + "dependencies": { + "p-wait-for": "6.0.0" }, "devDependencies": { "got": "14.6.6", "sinon": "21.0.3" - }, - "dependencies": { - "p-wait-for": "6.0.0" } } diff --git a/packages/webhook-mock-receiver/test/.eslintrc.js b/packages/webhook-mock-receiver/test/.eslintrc.js index ef13dee0e..c9b01755e 100644 --- a/packages/webhook-mock-receiver/test/.eslintrc.js +++ b/packages/webhook-mock-receiver/test/.eslintrc.js @@ -1,9 +1,7 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ], + plugins: ["ghost"], + extends: ["plugin:ghost/test"], globals: { - beforeAll: 'readonly' - } + beforeAll: "readonly", + }, }; diff --git a/packages/webhook-mock-receiver/test/WebhookMockReceiver.test.js b/packages/webhook-mock-receiver/test/WebhookMockReceiver.test.js index 5ee66d62f..460555d56 100644 --- a/packages/webhook-mock-receiver/test/WebhookMockReceiver.test.js +++ b/packages/webhook-mock-receiver/test/WebhookMockReceiver.test.js @@ -1,21 +1,21 @@ -const assert = require('assert/strict'); -const sinon = require('sinon'); +const assert = require("assert/strict"); +const sinon = require("sinon"); -const WebhookMockReceiver = require('../'); +const WebhookMockReceiver = require("../"); -describe('Webhook Mock Receiver', function () { +describe("Webhook Mock Receiver", function () { let snapshotManager; let webhookMockReceiver; let got; - const webhookURL = 'https://test-webhook-receiver.com/webhook'; + const webhookURL = "https://test-webhook-receiver.com/webhook"; beforeAll(async function () { - got = (await import('got')).default; + got = (await import("got")).default; snapshotManager = { - assertSnapshot: sinon.spy() + assertSnapshot: sinon.spy(), }; webhookMockReceiver = new WebhookMockReceiver({ - snapshotManager + snapshotManager, }); }); @@ -24,52 +24,55 @@ describe('Webhook Mock Receiver', function () { webhookMockReceiver.reset(); }); - describe('recordBodyResponse', function () { - it('saves the payload', function () { - webhookMockReceiver.recordRequest({foo: 'bar'}, { - headers: { - lorem: 'ipsum' - } - }); + describe("recordBodyResponse", function () { + it("saves the payload", function () { + webhookMockReceiver.recordRequest( + { foo: "bar" }, + { + headers: { + lorem: "ipsum", + }, + }, + ); assert.deepEqual(webhookMockReceiver.body, { body: { - foo: 'bar' - } + foo: "bar", + }, }); assert.deepEqual(webhookMockReceiver.headers, { headers: { - lorem: 'ipsum' - } + lorem: "ipsum", + }, }); }); }); - describe('mock', function () { - it('created a mock request receiver base on url', async function () { + describe("mock", function () { + it("created a mock request receiver base on url", async function () { webhookMockReceiver.mock(webhookURL); await got.post(webhookURL, { json: { - avocado: 'toast' - } + avocado: "toast", + }, }); assert.deepEqual(webhookMockReceiver.body, { body: { - avocado: 'toast' - } + avocado: "toast", + }, }); }); }); - describe('reset', function () { - it('resets the default state of the mock receiver', async function () { + describe("reset", function () { + it("resets the default state of the mock receiver", async function () { webhookMockReceiver.mock(webhookURL); await got.post(webhookURL, { headers: { - hey: 'ho' - } + hey: "ho", + }, }); assert.notEqual(webhookMockReceiver.body, undefined); @@ -82,18 +85,18 @@ describe('Webhook Mock Receiver', function () { }); }); - describe('receivedRequest', function () { - it('has has internal receivers done once receivedRequest resolves', async function () { + describe("receivedRequest", function () { + it("has has internal receivers done once receivedRequest resolves", async function () { webhookMockReceiver.mock(webhookURL); // shoot a request with a delay simulating request completion delay setTimeout(() => { got.post(webhookURL, { json: { - avocado: 'toast' - } + avocado: "toast", + }, }); - }, (10 + 1)); + }, 10 + 1); assert.equal(webhookMockReceiver._receiver.isDone(), false); @@ -103,13 +106,13 @@ describe('Webhook Mock Receiver', function () { }); }); - describe('matchBodySnapshot', function () { - it('checks the request payload', async function () { + describe("matchBodySnapshot", function () { + it("checks the request payload", async function () { webhookMockReceiver.mock(webhookURL); await got.post(webhookURL, { json: { - avocado: 'toast' - } + avocado: "toast", + }, }); webhookMockReceiver.matchBodySnapshot(); @@ -117,17 +120,20 @@ describe('Webhook Mock Receiver', function () { assert.equal(snapshotManager.assertSnapshot.calledOnce, true); assert.deepEqual(snapshotManager.assertSnapshot.args[0][0], { body: { - avocado: 'toast' - } + avocado: "toast", + }, }); - assert.deepEqual(snapshotManager.assertSnapshot.args[0][1].field, 'body'); - assert.deepEqual(snapshotManager.assertSnapshot.args[0][1].type, 'body'); + assert.deepEqual(snapshotManager.assertSnapshot.args[0][1].field, "body"); + assert.deepEqual(snapshotManager.assertSnapshot.args[0][1].type, "body"); assert.deepEqual(snapshotManager.assertSnapshot.args[0][1].properties, {}); - assert.deepEqual(snapshotManager.assertSnapshot.args[0][1].error.constructor.name, 'AssertionError'); + assert.deepEqual( + snapshotManager.assertSnapshot.args[0][1].error.constructor.name, + "AssertionError", + ); }); - it('waits for the request completion before checking the request payload', async function () { + it("waits for the request completion before checking the request payload", async function () { webhookMockReceiver.mock(webhookURL); // shoot a request with a delay simulating request completion delay @@ -135,10 +141,10 @@ describe('Webhook Mock Receiver', function () { setTimeout(() => { got.post(webhookURL, { json: { - avocado: 'toast' - } + avocado: "toast", + }, }); - }, (10 + 1)); + }, 10 + 1); await webhookMockReceiver.receivedRequest(); webhookMockReceiver.matchBodySnapshot(); @@ -146,45 +152,46 @@ describe('Webhook Mock Receiver', function () { assert.equal(snapshotManager.assertSnapshot.calledOnce, true); assert.deepEqual(snapshotManager.assertSnapshot.args[0][0], { body: { - avocado: 'toast' - } + avocado: "toast", + }, }); }); }); - describe('matchHeaderSnapshot', function () { - it('checks the request payload', async function () { + describe("matchHeaderSnapshot", function () { + it("checks the request payload", async function () { webhookMockReceiver.mock(webhookURL); await got.post(webhookURL, { headers: { - foo: 'bar' - } + foo: "bar", + }, }); await webhookMockReceiver.matchHeaderSnapshot(); assert.equal(snapshotManager.assertSnapshot.calledOnce, true); - assert.deepEqual(Object.keys(snapshotManager.assertSnapshot.args[0][0]), ['headers']); + assert.deepEqual(Object.keys(snapshotManager.assertSnapshot.args[0][0]), ["headers"]); const headers = snapshotManager.assertSnapshot.args[0][0].headers; - assert.equal(headers.foo, 'bar'); - assert.match(headers['accept-encoding'], /^gzip, deflate, br/); - assert.match(headers['user-agent'], /^got/); + assert.equal(headers.foo, "bar"); + assert.match(headers["accept-encoding"], /^gzip, deflate, br/); + assert.match(headers["user-agent"], /^got/); - assert.deepEqual(snapshotManager.assertSnapshot.args[0][1].field, 'headers'); - assert.deepEqual(snapshotManager.assertSnapshot.args[0][1].type, 'header'); + assert.deepEqual(snapshotManager.assertSnapshot.args[0][1].field, "headers"); + assert.deepEqual(snapshotManager.assertSnapshot.args[0][1].type, "header"); assert.deepEqual(snapshotManager.assertSnapshot.args[0][1].properties, {}); - assert.deepEqual(snapshotManager.assertSnapshot.args[0][1].error.constructor.name, 'AssertionError'); + assert.deepEqual( + snapshotManager.assertSnapshot.args[0][1].error.constructor.name, + "AssertionError", + ); }); }); - describe('chainable interface', function () { - it('chains body and header snapshot matching assertions', async function () { + describe("chainable interface", function () { + it("chains body and header snapshot matching assertions", async function () { webhookMockReceiver.mock(webhookURL); await got.post(webhookURL); - webhookMockReceiver - .matchHeaderSnapshot() - .matchBodySnapshot(); + webhookMockReceiver.matchHeaderSnapshot().matchBodySnapshot(); }); }); }); diff --git a/packages/zip/README.md b/packages/zip/README.md index 8d4aa88b0..b008526bc 100644 --- a/packages/zip/README.md +++ b/packages/zip/README.md @@ -8,7 +8,6 @@ or `yarn add @tryghost/zip` - ## Purpose Zip compression and extraction utilities with safety checks for symlinks and unsafe filenames. @@ -32,23 +31,19 @@ let res = await zip.extract('path/to/archive.zip', 'path/to/files', [options]) This is a mono repository, managed with [lerna](https://lernajs.io/). Follow the instructions for the top-level repo. + 1. `git clone` this repo & `cd` into it as usual 2. Run `yarn` to install top-level dependencies. - ## Run - `yarn dev` - ## Test - `yarn lint` run just eslint - `yarn test` run lint and tests - - - # Copyright & License Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/packages/zip/index.js b/packages/zip/index.js index 75700f7c5..475810c31 100644 --- a/packages/zip/index.js +++ b/packages/zip/index.js @@ -1,4 +1,4 @@ module.exports = { - extract: require('./lib/extract'), - compress: require('./lib/compress') + extract: require("./lib/extract"), + compress: require("./lib/compress"), }; diff --git a/packages/zip/lib/compress.js b/packages/zip/lib/compress.js index 7cd20d522..749241493 100644 --- a/packages/zip/lib/compress.js +++ b/packages/zip/lib/compress.js @@ -1,10 +1,10 @@ -const fs = require('fs'); +const fs = require("fs"); const defaultOptions = { - type: 'zip', - glob: '**/*', + type: "zip", + glob: "**/*", dot: true, - ignore: ['node_modules/**'] + ignore: ["node_modules/**"], }; /** @@ -24,7 +24,7 @@ const defaultOptions = { module.exports = (folderToZip, destination, options = {}) => { const opts = Object.assign({}, defaultOptions, options); - const archiver = require('archiver'); + const archiver = require("archiver"); const output = fs.createWriteStream(destination); const archive = archiver.create(opts.type); @@ -35,17 +35,17 @@ module.exports = (folderToZip, destination, options = {}) => { folderToZip = fs.realpathSync(folderToZip); } - output.on('close', function () { - resolve({path: destination, size: archive.pointer()}); + output.on("close", function () { + resolve({ path: destination, size: archive.pointer() }); }); - archive.on('error', function (err) { + archive.on("error", function (err) { reject(err); }); archive.glob(opts.glob, { cwd: folderToZip, dot: opts.dot, - ignore: opts.ignore + ignore: opts.ignore, }); archive.pipe(output); archive.finalize(); diff --git a/packages/zip/lib/extract.js b/packages/zip/lib/extract.js index 6ac083ab3..22e28cba3 100644 --- a/packages/zip/lib/extract.js +++ b/packages/zip/lib/extract.js @@ -1,10 +1,10 @@ -const errors = require('@tryghost/errors'); +const errors = require("@tryghost/errors"); const defaultOptions = {}; function throwOnSymlinks(entry) { // Check if symlink - const mode = (entry.externalFileAttributes >> 16) & 0xFFFF; + const mode = (entry.externalFileAttributes >> 16) & 0xffff; // check if it's a symlink or dir (using stat mode constants) const IFMT = 61440; const IFLNK = 40960; @@ -12,15 +12,15 @@ function throwOnSymlinks(entry) { if (symlink) { throw new errors.UnsupportedMediaTypeError({ - message: 'Symlinks are not allowed in the zip folder.' + message: "Symlinks are not allowed in the zip folder.", }); } } function throwOnLargeFilenames(entry) { - if (Buffer.byteLength(entry.fileName, 'utf8') >= 254) { + if (Buffer.byteLength(entry.fileName, "utf8") >= 254) { throw new errors.UnsupportedMediaTypeError({ - message: 'File names in the zip folder must be shorter than 254 characters.' + message: "File names in the zip folder must be shorter than 254 characters.", }); } } @@ -40,7 +40,7 @@ function throwOnLargeFilenames(entry) { module.exports = (zipToExtract, destination, options) => { const opts = Object.assign({}, defaultOptions, options); - const extract = require('extract-zip'); + const extract = require("extract-zip"); opts.dir = destination; @@ -54,6 +54,6 @@ module.exports = (zipToExtract, destination, options) => { }; return extract(zipToExtract, opts).then(() => { - return {path: destination}; + return { path: destination }; }); }; diff --git a/packages/zip/package.json b/packages/zip/package.json index 79a5e673a..cf9e86a99 100644 --- a/packages/zip/package.json +++ b/packages/zip/package.json @@ -1,35 +1,35 @@ { "name": "@tryghost/zip", "version": "3.0.3", + "license": "MIT", + "author": "Ghost Foundation", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", "directory": "packages/zip" }, - "author": "Ghost Foundation", - "license": "MIT", - "main": "index.js", - "scripts": { - "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" - }, "files": [ "index.js", "lib" ], + "main": "index.js", "publishConfig": { "access": "public" }, - "devDependencies": { - "folder-hash": "4.1.2", - "sinon": "21.0.3" + "scripts": { + "dev": "echo \"Implement me!\"", + "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", + "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "posttest": "yarn lint", + "lint:eslint": "eslint . --ext .js --cache" }, "dependencies": { "@tryghost/errors": "^3.0.3", "archiver": "7.0.1", "extract-zip": "2.0.1" + }, + "devDependencies": { + "folder-hash": "4.1.2", + "sinon": "21.0.3" } } diff --git a/packages/zip/test/.eslintrc.js b/packages/zip/test/.eslintrc.js index c0a7056a1..563365ce4 100644 --- a/packages/zip/test/.eslintrc.js +++ b/packages/zip/test/.eslintrc.js @@ -1,10 +1,8 @@ module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ], + plugins: ["ghost"], + extends: ["plugin:ghost/test"], globals: { - beforeAll: 'readonly', - afterAll: 'readonly' - } + beforeAll: "readonly", + afterAll: "readonly", + }, }; diff --git a/packages/zip/test/zip.test.js b/packages/zip/test/zip.test.js index e43b09eb2..c97c9ba84 100644 --- a/packages/zip/test/zip.test.js +++ b/packages/zip/test/zip.test.js @@ -1,28 +1,28 @@ -const assert = require('assert/strict'); -const path = require('path'); -const fs = require('fs'); -const {hashElement} = require('folder-hash'); -const archiver = require('archiver'); -const EventEmitter = require('events'); -const Module = require('module'); +const assert = require("assert/strict"); +const path = require("path"); +const fs = require("fs"); +const { hashElement } = require("folder-hash"); +const archiver = require("archiver"); +const EventEmitter = require("events"); +const Module = require("module"); // Mimic how we expect this to be required -const {compress, extract} = require('../'); +const { compress, extract } = require("../"); -describe('Compress and Extract should be opposite functions', function () { +describe("Compress and Extract should be opposite functions", function () { let symlinkPath, themeFolder, zipDestination, unzipDestination; const cleanUp = () => { - fs.rmSync(symlinkPath, {recursive: true, force: true}); - fs.rmSync(zipDestination, {recursive: true, force: true}); - fs.rmSync(unzipDestination, {recursive: true, force: true}); + fs.rmSync(symlinkPath, { recursive: true, force: true }); + fs.rmSync(zipDestination, { recursive: true, force: true }); + fs.rmSync(unzipDestination, { recursive: true, force: true }); }; beforeAll(function () { - symlinkPath = path.join(__dirname, 'fixtures', 'theme-symlink'); - themeFolder = path.join(__dirname, 'fixtures', 'test-theme'); - zipDestination = path.join(__dirname, 'fixtures', 'test-theme.zip'); - unzipDestination = path.join(__dirname, 'fixtures', 'test-theme-unzipped'); + symlinkPath = path.join(__dirname, "fixtures", "theme-symlink"); + themeFolder = path.join(__dirname, "fixtures", "test-theme"); + zipDestination = path.join(__dirname, "fixtures", "test-theme.zip"); + unzipDestination = path.join(__dirname, "fixtures", "test-theme-unzipped"); cleanUp(); }); @@ -31,38 +31,38 @@ describe('Compress and Extract should be opposite functions', function () { cleanUp(); }); - it('ensure symlinks work', async function () { + it("ensure symlinks work", async function () { fs.symlinkSync(themeFolder, symlinkPath); const originalHash = await hashElement(symlinkPath); const compressRes = await compress(symlinkPath, zipDestination); - assert.equal(typeof compressRes, 'object'); + assert.equal(typeof compressRes, "object"); assert.equal(compressRes.path, zipDestination); assert.equal(compressRes.size < 619618, true); const extractRes = await extract(zipDestination, unzipDestination); - assert.equal(typeof extractRes, 'object'); + assert.equal(typeof extractRes, "object"); assert.equal(extractRes.path, unzipDestination); const extractedHash = await hashElement(unzipDestination); assert.equal(originalHash.children.toString(), extractedHash.children.toString()); }); - it('rejects when archiver emits an async error event', async function () { + it("rejects when archiver emits an async error event", async function () { const originalLoad = Module._load; Module._load = function (request, parent, isMain) { - if (request === 'archiver') { + if (request === "archiver") { return { create() { const fake = new EventEmitter(); fake.glob = function () {}; fake.pipe = function () {}; fake.finalize = function () { - setTimeout(() => fake.emit('error', new Error('archive failed')), 0); + setTimeout(() => fake.emit("error", new Error("archive failed")), 0); }; return fake; - } + }, }; } @@ -70,92 +70,89 @@ describe('Compress and Extract should be opposite functions', function () { }; try { - await assert.rejects( - () => compress(themeFolder, zipDestination), - /archive failed/ - ); + await assert.rejects(() => compress(themeFolder, zipDestination), /archive failed/); } finally { Module._load = originalLoad; } }); }); -describe('Extract zip', function () { +describe("Extract zip", function () { let themeFolder, zipDestination, unzipDestination, symLinkPath, longFilePath; beforeAll(function () { - themeFolder = path.join(__dirname, 'fixtures', 'test-theme'); - zipDestination = path.join(__dirname, 'fixtures', 'test-theme.zip'); - unzipDestination = path.join(__dirname, 'fixtures', 'test-theme-unzipped'); - symLinkPath = path.join(__dirname, 'fixtures', 'test-theme-symlink'); + themeFolder = path.join(__dirname, "fixtures", "test-theme"); + zipDestination = path.join(__dirname, "fixtures", "test-theme.zip"); + unzipDestination = path.join(__dirname, "fixtures", "test-theme-unzipped"); + symLinkPath = path.join(__dirname, "fixtures", "test-theme-symlink"); }); afterEach(function () { if (fs.existsSync(zipDestination)) { - fs.rmSync(zipDestination, {recursive: true, force: true}); + fs.rmSync(zipDestination, { recursive: true, force: true }); } if (fs.existsSync(unzipDestination)) { - fs.rmSync(unzipDestination, {recursive: true, force: true}); + fs.rmSync(unzipDestination, { recursive: true, force: true }); } if (fs.existsSync(symLinkPath)) { - fs.rmSync(symLinkPath, {recursive: true, force: true}); + fs.rmSync(symLinkPath, { recursive: true, force: true }); } if (fs.existsSync(longFilePath)) { - fs.rmSync(longFilePath, {recursive: true, force: true}); + fs.rmSync(longFilePath, { recursive: true, force: true }); } }); - it('extracts a zip file', async function () { + it("extracts a zip file", async function () { await compress(themeFolder, zipDestination); await extract(zipDestination, unzipDestination); assert.equal(fs.existsSync(unzipDestination), true); - assert.equal(fs.existsSync(path.join(unzipDestination, 'package.json')), true); + assert.equal(fs.existsSync(path.join(unzipDestination, "package.json")), true); }); - it('throws if the zip contains a filename with 254 or more bytes', async function () { - const longFileName = 'a'.repeat(250) + '.txt'; // 254 bytes + it("throws if the zip contains a filename with 254 or more bytes", async function () { + const longFileName = "a".repeat(250) + ".txt"; // 254 bytes longFilePath = path.join(themeFolder, longFileName); - fs.writeFileSync(longFilePath, 'test content'); + fs.writeFileSync(longFilePath, "test content"); await compress(themeFolder, zipDestination); await assert.rejects( () => extract(zipDestination, unzipDestination), - /File names in the zip folder must be shorter than 254 characters\./ + /File names in the zip folder must be shorter than 254 characters\./, ); }); - it('throws when the zip contains symlink entries', async function () { + it("throws when the zip contains symlink entries", async function () { await new Promise((resolve, reject) => { const output = fs.createWriteStream(zipDestination); - const archive = archiver('zip'); + const archive = archiver("zip"); - output.on('close', resolve); - archive.on('error', reject); + output.on("close", resolve); + archive.on("error", reject); archive.pipe(output); - archive.symlink('themes/test-target', 'symlink-entry'); + archive.symlink("themes/test-target", "symlink-entry"); archive.finalize(); }); await assert.rejects( () => extract(zipDestination, unzipDestination), - /Symlinks are not allowed in the zip folder\./ + /Symlinks are not allowed in the zip folder\./, ); }); - it('forwards custom onEntry callback', async function () { + it("forwards custom onEntry callback", async function () { await compress(themeFolder, zipDestination); let called = false; await extract(zipDestination, unzipDestination, { onEntry: () => { called = true; - } + }, }); assert.equal(called, true); From 18a55d2a7ecbfbb4ccaaebef6770320b4968a49c Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Tue, 24 Mar 2026 07:12:37 -0500 Subject: [PATCH 3/5] chore: make oxlint the default lint path --- README.md | 2 +- package.json | 4 ++-- packages/api-framework/package.json | 2 +- packages/bookshelf-collision/package.json | 2 +- packages/bookshelf-custom-query/package.json | 2 +- packages/bookshelf-eager-load/package.json | 2 +- packages/bookshelf-filter/package.json | 2 +- packages/bookshelf-has-posts/package.json | 2 +- packages/bookshelf-include-count/package.json | 2 +- packages/bookshelf-order/package.json | 2 +- packages/bookshelf-pagination/package.json | 2 +- packages/bookshelf-plugins/package.json | 2 +- packages/bookshelf-search/package.json | 2 +- packages/bookshelf-transaction-events/package.json | 2 +- packages/config/package.json | 2 +- packages/database-info/package.json | 2 +- packages/debug/package.json | 2 +- packages/domain-events/package.json | 2 +- packages/elasticsearch/package.json | 2 +- packages/email-mock-receiver/package.json | 2 +- packages/errors/package.json | 2 +- packages/express-test/package.json | 2 +- packages/http-cache-utils/package.json | 2 +- packages/http-stream/package.json | 2 +- packages/jest-snapshot/package.json | 2 +- packages/job-manager/package.json | 2 +- packages/logging/package.json | 2 +- packages/metrics/package.json | 2 +- packages/mw-error-handler/package.json | 2 +- packages/mw-vhost/package.json | 2 +- packages/nodemailer/package.json | 2 +- packages/pretty-cli/package.json | 2 +- packages/pretty-stream/package.json | 2 +- packages/prometheus-metrics/package.json | 2 +- packages/promise/package.json | 2 +- packages/request/package.json | 2 +- packages/root-utils/package.json | 2 +- packages/security/package.json | 2 +- packages/server/package.json | 2 +- packages/tpl/package.json | 2 +- packages/validator/package.json | 2 +- packages/version/package.json | 2 +- packages/webhook-mock-receiver/package.json | 2 +- packages/zip/package.json | 2 +- 44 files changed, 45 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 136bcad43..69b53b4d3 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ To add a new package to the repo: ## Test -- `yarn lint` run `oxlint` first, then existing ESLint checks +- `yarn lint` run `oxlint` only (default lint path) - `yarn lint:oxlint` run `oxlint` only - `yarn lint:eslint` run ESLint-only compatibility checks - `yarn format` format `js/ts/json/md` files with `oxfmt` diff --git a/package.json b/package.json index 5c53c8327..dc70fd486 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "setup": "yarn", "test": "nx run-many -t test --parallel=10 --outputStyle=dynamic-legacy", "test:ci": "nx run-many -t test --outputStyle=dynamic", - "lint": "yarn lint:oxlint && yarn lint:eslint", + "lint": "yarn lint:oxlint", "preship": "git diff --quiet && git diff --cached --quiet || (echo 'Error: working tree must be clean before shipping' && exit 1) && yarn test", "ship": "nx release version --git-push --git-remote ${GHOST_UPSTREAM:-origin}", "ship:patch": "yarn ship patch", @@ -27,7 +27,7 @@ "ship:major": "yarn ship major", "ship:first-release": "yarn ship patch --first-release", "lint:eslint": "nx run-many -t lint:eslint --outputStyle=dynamic", - "lint:oxlint": "oxlint -c .oxlintrc.json packages", + "lint:oxlint": "nx run-many -t lint --outputStyle=dynamic", "format": "oxfmt -c .oxfmtrc.json \"packages/**/*.{js,ts,json,md}\"", "format:check": "oxfmt -c .oxfmtrc.json --check \"packages/**/*.{js,ts,json,md}\"" }, diff --git a/packages/api-framework/package.json b/packages/api-framework/package.json index e00da77ce..e03bbc4f9 100644 --- a/packages/api-framework/package.json +++ b/packages/api-framework/package.json @@ -20,7 +20,7 @@ "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "test": "yarn test:unit", "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", "lint:eslint": "yarn lint:code && yarn lint:test" }, diff --git a/packages/bookshelf-collision/package.json b/packages/bookshelf-collision/package.json index 3336f6838..f938d9000 100644 --- a/packages/bookshelf-collision/package.json +++ b/packages/bookshelf-collision/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/bookshelf-custom-query/package.json b/packages/bookshelf-custom-query/package.json index 82894c59e..2031fe8f9 100644 --- a/packages/bookshelf-custom-query/package.json +++ b/packages/bookshelf-custom-query/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/bookshelf-eager-load/package.json b/packages/bookshelf-eager-load/package.json index 02365fa53..17e84d170 100644 --- a/packages/bookshelf-eager-load/package.json +++ b/packages/bookshelf-eager-load/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/bookshelf-filter/package.json b/packages/bookshelf-filter/package.json index 733836c37..31ee970ee 100644 --- a/packages/bookshelf-filter/package.json +++ b/packages/bookshelf-filter/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/bookshelf-has-posts/package.json b/packages/bookshelf-has-posts/package.json index 08b752172..e9f03c0cd 100644 --- a/packages/bookshelf-has-posts/package.json +++ b/packages/bookshelf-has-posts/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/bookshelf-include-count/package.json b/packages/bookshelf-include-count/package.json index 0c2bf2712..de7fec5eb 100644 --- a/packages/bookshelf-include-count/package.json +++ b/packages/bookshelf-include-count/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/bookshelf-order/package.json b/packages/bookshelf-order/package.json index 28848cad6..2a9b13695 100644 --- a/packages/bookshelf-order/package.json +++ b/packages/bookshelf-order/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/bookshelf-pagination/package.json b/packages/bookshelf-pagination/package.json index 8e7b98393..52c9931db 100644 --- a/packages/bookshelf-pagination/package.json +++ b/packages/bookshelf-pagination/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/bookshelf-plugins/package.json b/packages/bookshelf-plugins/package.json index c2fa3d617..b5f8fa523 100644 --- a/packages/bookshelf-plugins/package.json +++ b/packages/bookshelf-plugins/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/bookshelf-search/package.json b/packages/bookshelf-search/package.json index 4fa81b5b9..ce69f87cd 100644 --- a/packages/bookshelf-search/package.json +++ b/packages/bookshelf-search/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/bookshelf-transaction-events/package.json b/packages/bookshelf-transaction-events/package.json index 1e51cdcd7..a7ecd8b9d 100644 --- a/packages/bookshelf-transaction-events/package.json +++ b/packages/bookshelf-transaction-events/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/config/package.json b/packages/config/package.json index 7537247f4..0cb34d093 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/database-info/package.json b/packages/database-info/package.json index 3347858f2..acb5cb297 100644 --- a/packages/database-info/package.json +++ b/packages/database-info/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/debug/package.json b/packages/debug/package.json index 7cacb1d9d..9a801d343 100644 --- a/packages/debug/package.json +++ b/packages/debug/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/domain-events/package.json b/packages/domain-events/package.json index 7494317bd..fd7811eeb 100644 --- a/packages/domain-events/package.json +++ b/packages/domain-events/package.json @@ -22,7 +22,7 @@ "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "test": "yarn test:unit", "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", "lint:eslint": "yarn lint:code && yarn lint:test" }, diff --git a/packages/elasticsearch/package.json b/packages/elasticsearch/package.json index cabb2f11f..20ef047e3 100644 --- a/packages/elasticsearch/package.json +++ b/packages/elasticsearch/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/email-mock-receiver/package.json b/packages/email-mock-receiver/package.json index 243cd1554..1fa123d24 100644 --- a/packages/email-mock-receiver/package.json +++ b/packages/email-mock-receiver/package.json @@ -21,7 +21,7 @@ "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "test": "yarn test:unit", "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", "lint:eslint": "yarn lint:code && yarn lint:test" }, diff --git a/packages/errors/package.json b/packages/errors/package.json index d5cf25575..2775196e6 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -31,7 +31,7 @@ "build:es": "esbuild src/*.ts --target=es2020 --outdir=es --format=esm", "build:types": "tsc --emitDeclarationOnly --declaration --declarationMap --outDir types", "test": "NODE_ENV=testing vitest run --coverage", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/express-test/package.json b/packages/express-test/package.json index 965916e58..e4f773330 100644 --- a/packages/express-test/package.json +++ b/packages/express-test/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/http-cache-utils/package.json b/packages/http-cache-utils/package.json index d82c6252c..508e0bc00 100644 --- a/packages/http-cache-utils/package.json +++ b/packages/http-cache-utils/package.json @@ -21,7 +21,7 @@ "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "test": "yarn test:unit", "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", "lint:eslint": "yarn lint:code && yarn lint:test" }, diff --git a/packages/http-stream/package.json b/packages/http-stream/package.json index 59f75eac3..e02417676 100644 --- a/packages/http-stream/package.json +++ b/packages/http-stream/package.json @@ -20,7 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", "posttest": "yarn lint", "lint:eslint": "yarn lint:code && yarn lint:test" diff --git a/packages/jest-snapshot/package.json b/packages/jest-snapshot/package.json index 3a9a8c362..5c78ca2d4 100644 --- a/packages/jest-snapshot/package.json +++ b/packages/jest-snapshot/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/job-manager/package.json b/packages/job-manager/package.json index 9e5e2a7f0..0be616b90 100644 --- a/packages/job-manager/package.json +++ b/packages/job-manager/package.json @@ -21,7 +21,7 @@ "test:unit": "NODE_ENV=testing vitest run --coverage", "test": "yarn test:unit", "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", "lint:eslint": "yarn lint:code && yarn lint:test" }, diff --git a/packages/logging/package.json b/packages/logging/package.json index a63582490..7dcff8e7d 100644 --- a/packages/logging/package.json +++ b/packages/logging/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/metrics/package.json b/packages/metrics/package.json index 163538f41..ac8733b0a 100644 --- a/packages/metrics/package.json +++ b/packages/metrics/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/mw-error-handler/package.json b/packages/mw-error-handler/package.json index 8456c45b6..90fa778d6 100644 --- a/packages/mw-error-handler/package.json +++ b/packages/mw-error-handler/package.json @@ -22,7 +22,7 @@ "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "test": "yarn test:unit", "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", "lint:eslint": "yarn lint:code && yarn lint:test" }, diff --git a/packages/mw-vhost/package.json b/packages/mw-vhost/package.json index a983425f4..796fd2cfd 100644 --- a/packages/mw-vhost/package.json +++ b/packages/mw-vhost/package.json @@ -21,7 +21,7 @@ "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "test": "yarn test:unit", "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", "lint:eslint": "yarn lint:code && yarn lint:test" }, diff --git a/packages/nodemailer/package.json b/packages/nodemailer/package.json index 3fa522e50..5b43da6b8 100644 --- a/packages/nodemailer/package.json +++ b/packages/nodemailer/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/pretty-cli/package.json b/packages/pretty-cli/package.json index 63a9683f9..8e7489ba2 100644 --- a/packages/pretty-cli/package.json +++ b/packages/pretty-cli/package.json @@ -20,7 +20,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/pretty-stream/package.json b/packages/pretty-stream/package.json index 663935926..53a241efd 100644 --- a/packages/pretty-stream/package.json +++ b/packages/pretty-stream/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/prometheus-metrics/package.json b/packages/prometheus-metrics/package.json index 9743feb7b..1d5246819 100644 --- a/packages/prometheus-metrics/package.json +++ b/packages/prometheus-metrics/package.json @@ -24,7 +24,7 @@ "test": "yarn test:types && yarn test:unit", "test:types": "tsc --noEmit", "lint:code": "eslint src/ --ext .ts --cache", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "lint:test": "eslint -c test/.eslintrc.js test/ --ext .ts --cache", "lint:eslint": "yarn lint:code && yarn lint:test" }, diff --git a/packages/promise/package.json b/packages/promise/package.json index 93f8cb41b..cad289f16 100644 --- a/packages/promise/package.json +++ b/packages/promise/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/request/package.json b/packages/request/package.json index 6fc9bfb52..b6945f78e 100644 --- a/packages/request/package.json +++ b/packages/request/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/root-utils/package.json b/packages/root-utils/package.json index cb0c0f2df..f2d56a10a 100644 --- a/packages/root-utils/package.json +++ b/packages/root-utils/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/security/package.json b/packages/security/package.json index cd9da8f1b..023d6b454 100644 --- a/packages/security/package.json +++ b/packages/security/package.json @@ -22,7 +22,7 @@ "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "test": "yarn test:unit", "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", "lint:eslint": "yarn lint:code && yarn lint:test" }, diff --git a/packages/server/package.json b/packages/server/package.json index 5d5f84f1c..8b4a764c9 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/tpl/package.json b/packages/tpl/package.json index c38b1e692..e4a22b074 100644 --- a/packages/tpl/package.json +++ b/packages/tpl/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/validator/package.json b/packages/validator/package.json index 49c25fce3..cca0f33b9 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/version/package.json b/packages/version/package.json index 2c9495481..c16298244 100644 --- a/packages/version/package.json +++ b/packages/version/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, diff --git a/packages/webhook-mock-receiver/package.json b/packages/webhook-mock-receiver/package.json index 4bd3235d0..3fd88c448 100644 --- a/packages/webhook-mock-receiver/package.json +++ b/packages/webhook-mock-receiver/package.json @@ -20,7 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", "posttest": "yarn lint", "lint:eslint": "yarn lint:code && yarn lint:test" diff --git a/packages/zip/package.json b/packages/zip/package.json index cf9e86a99..55044aa56 100644 --- a/packages/zip/package.json +++ b/packages/zip/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint": "oxlint -c ../../.oxlintrc.json . && yarn lint:eslint", + "lint": "oxlint -c ../../.oxlintrc.json .", "posttest": "yarn lint", "lint:eslint": "eslint . --ext .js --cache" }, From 640cefb993aa16e864cdf50c0db7b6118840d932 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Tue, 24 Mar 2026 07:19:36 -0500 Subject: [PATCH 4/5] chore: remove eslint lint path and add custom ghost checks --- README.md | 4 +- package.json | 11 +- packages/api-framework/package.json | 5 +- packages/bookshelf-collision/package.json | 3 +- packages/bookshelf-custom-query/package.json | 3 +- packages/bookshelf-eager-load/package.json | 3 +- packages/bookshelf-filter/package.json | 3 +- packages/bookshelf-has-posts/package.json | 3 +- packages/bookshelf-include-count/package.json | 3 +- packages/bookshelf-order/package.json | 3 +- packages/bookshelf-pagination/package.json | 3 +- packages/bookshelf-plugins/package.json | 3 +- packages/bookshelf-search/package.json | 3 +- .../bookshelf-transaction-events/package.json | 3 +- packages/config/package.json | 3 +- packages/database-info/package.json | 3 +- packages/debug/package.json | 3 +- packages/domain-events/package.json | 5 +- packages/elasticsearch/package.json | 3 +- packages/email-mock-receiver/package.json | 5 +- packages/errors/package.json | 3 +- packages/express-test/package.json | 3 +- packages/http-cache-utils/package.json | 5 +- packages/http-stream/package.json | 5 +- packages/jest-snapshot/package.json | 3 +- packages/job-manager/package.json | 5 +- packages/logging/package.json | 3 +- packages/metrics/package.json | 3 +- packages/mw-error-handler/package.json | 5 +- packages/mw-vhost/package.json | 5 +- packages/nodemailer/package.json | 3 +- packages/pretty-cli/package.json | 3 +- packages/pretty-stream/package.json | 3 +- packages/prometheus-metrics/package.json | 5 +- packages/promise/package.json | 3 +- packages/request/package.json | 3 +- packages/root-utils/package.json | 3 +- packages/security/package.json | 5 +- packages/server/package.json | 3 +- packages/tpl/package.json | 3 +- packages/validator/package.json | 3 +- packages/version/package.json | 3 +- packages/webhook-mock-receiver/package.json | 5 +- packages/zip/package.json | 3 +- scripts/lint-custom-rules.js | 267 ++++++++++++++++++ 45 files changed, 314 insertions(+), 116 deletions(-) create mode 100644 scripts/lint-custom-rules.js diff --git a/README.md b/README.md index 69b53b4d3..3c6866a51 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,9 @@ To add a new package to the repo: ## Test -- `yarn lint` run `oxlint` only (default lint path) +- `yarn lint` run `oxlint` plus custom Ghost-specific lint checks - `yarn lint:oxlint` run `oxlint` only -- `yarn lint:eslint` run ESLint-only compatibility checks +- `yarn lint:custom` run custom Ghost-specific lint checks - `yarn format` format `js/ts/json/md` files with `oxfmt` - `yarn format:check` check `js/ts/json/md` formatting with `oxfmt` - `yarn test` run tests (many packages still run lint in `posttest`) diff --git a/package.json b/package.json index dc70fd486..913e3558b 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,6 @@ "workspaces": [ "packages/*" ], - "eslintIgnore": [ - "**/node_modules/**" - ], "scripts": { "dev": "echo \"Implement me!\"", "main": "git checkout main && git pull && yarn", @@ -19,23 +16,21 @@ "setup": "yarn", "test": "nx run-many -t test --parallel=10 --outputStyle=dynamic-legacy", "test:ci": "nx run-many -t test --outputStyle=dynamic", - "lint": "yarn lint:oxlint", + "lint": "yarn lint:oxlint && yarn lint:custom", "preship": "git diff --quiet && git diff --cached --quiet || (echo 'Error: working tree must be clean before shipping' && exit 1) && yarn test", "ship": "nx release version --git-push --git-remote ${GHOST_UPSTREAM:-origin}", "ship:patch": "yarn ship patch", "ship:minor": "yarn ship minor", "ship:major": "yarn ship major", "ship:first-release": "yarn ship patch --first-release", - "lint:eslint": "nx run-many -t lint:eslint --outputStyle=dynamic", "lint:oxlint": "nx run-many -t lint --outputStyle=dynamic", "format": "oxfmt -c .oxfmtrc.json \"packages/**/*.{js,ts,json,md}\"", - "format:check": "oxfmt -c .oxfmtrc.json --check \"packages/**/*.{js,ts,json,md}\"" + "format:check": "oxfmt -c .oxfmtrc.json --check \"packages/**/*.{js,ts,json,md}\"", + "lint:custom": "node scripts/lint-custom-rules.js" }, "devDependencies": { "@nx/js": "22.6.1", "@vitest/coverage-v8": "3.2.4", - "eslint": "8.57.1", - "eslint-plugin-ghost": "3.5.0", "nx": "22.6.1", "oxfmt": "0.41.0", "oxlint": "1.56.0", diff --git a/packages/api-framework/package.json b/packages/api-framework/package.json index e03bbc4f9..5e2011e33 100644 --- a/packages/api-framework/package.json +++ b/packages/api-framework/package.json @@ -19,10 +19,7 @@ "dev": "echo \"Implement me!\"", "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "test": "yarn test:unit", - "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "oxlint -c ../../.oxlintrc.json .", - "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", - "lint:eslint": "yarn lint:code && yarn lint:test" + "lint": "oxlint -c ../../.oxlintrc.json ." }, "dependencies": { "@tryghost/debug": "^2.0.3", diff --git a/packages/bookshelf-collision/package.json b/packages/bookshelf-collision/package.json index f938d9000..837695c5d 100644 --- a/packages/bookshelf-collision/package.json +++ b/packages/bookshelf-collision/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "dependencies": { "@tryghost/errors": "^3.0.3", diff --git a/packages/bookshelf-custom-query/package.json b/packages/bookshelf-custom-query/package.json index 2031fe8f9..e061e9708 100644 --- a/packages/bookshelf-custom-query/package.json +++ b/packages/bookshelf-custom-query/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "devDependencies": { "sinon": "21.0.3" diff --git a/packages/bookshelf-eager-load/package.json b/packages/bookshelf-eager-load/package.json index 17e84d170..2ec7624ec 100644 --- a/packages/bookshelf-eager-load/package.json +++ b/packages/bookshelf-eager-load/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "dependencies": { "@tryghost/debug": "^2.0.3", diff --git a/packages/bookshelf-filter/package.json b/packages/bookshelf-filter/package.json index 31ee970ee..bd26880dd 100644 --- a/packages/bookshelf-filter/package.json +++ b/packages/bookshelf-filter/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "dependencies": { "@tryghost/debug": "^2.0.3", diff --git a/packages/bookshelf-has-posts/package.json b/packages/bookshelf-has-posts/package.json index e9f03c0cd..475d904be 100644 --- a/packages/bookshelf-has-posts/package.json +++ b/packages/bookshelf-has-posts/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "dependencies": { "@tryghost/debug": "^2.0.3", diff --git a/packages/bookshelf-include-count/package.json b/packages/bookshelf-include-count/package.json index de7fec5eb..9e3596c2f 100644 --- a/packages/bookshelf-include-count/package.json +++ b/packages/bookshelf-include-count/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "dependencies": { "@tryghost/debug": "^2.0.3", diff --git a/packages/bookshelf-order/package.json b/packages/bookshelf-order/package.json index 2a9b13695..b0b91dcc1 100644 --- a/packages/bookshelf-order/package.json +++ b/packages/bookshelf-order/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "dependencies": { "lodash": "4.17.23" diff --git a/packages/bookshelf-pagination/package.json b/packages/bookshelf-pagination/package.json index 52c9931db..434dd5dcb 100644 --- a/packages/bookshelf-pagination/package.json +++ b/packages/bookshelf-pagination/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "dependencies": { "@tryghost/errors": "^3.0.3", diff --git a/packages/bookshelf-plugins/package.json b/packages/bookshelf-plugins/package.json index b5f8fa523..35c4c75e1 100644 --- a/packages/bookshelf-plugins/package.json +++ b/packages/bookshelf-plugins/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "dependencies": { "@tryghost/bookshelf-collision": "^2.0.3", diff --git a/packages/bookshelf-search/package.json b/packages/bookshelf-search/package.json index ce69f87cd..d5fe73400 100644 --- a/packages/bookshelf-search/package.json +++ b/packages/bookshelf-search/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "devDependencies": { "sinon": "21.0.3" diff --git a/packages/bookshelf-transaction-events/package.json b/packages/bookshelf-transaction-events/package.json index a7ecd8b9d..e533fc985 100644 --- a/packages/bookshelf-transaction-events/package.json +++ b/packages/bookshelf-transaction-events/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "devDependencies": { "sinon": "21.0.3" diff --git a/packages/config/package.json b/packages/config/package.json index 0cb34d093..9cf1c28f2 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "dependencies": { "@tryghost/root-utils": "^2.0.3", diff --git a/packages/database-info/package.json b/packages/database-info/package.json index acb5cb297..6b2999195 100644 --- a/packages/database-info/package.json +++ b/packages/database-info/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "devDependencies": { "knex": "3.1.0" diff --git a/packages/debug/package.json b/packages/debug/package.json index 9a801d343..b60982f10 100644 --- a/packages/debug/package.json +++ b/packages/debug/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "dependencies": { "@tryghost/root-utils": "^2.0.3", diff --git a/packages/domain-events/package.json b/packages/domain-events/package.json index fd7811eeb..6101ccebe 100644 --- a/packages/domain-events/package.json +++ b/packages/domain-events/package.json @@ -21,10 +21,7 @@ "dev": "echo \"Implement me!\"", "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "test": "yarn test:unit", - "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "oxlint -c ../../.oxlintrc.json .", - "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", - "lint:eslint": "yarn lint:code && yarn lint:test" + "lint": "oxlint -c ../../.oxlintrc.json ." }, "devDependencies": { "@tryghost/logging": "^4.0.3" diff --git a/packages/elasticsearch/package.json b/packages/elasticsearch/package.json index 20ef047e3..d5134dae4 100644 --- a/packages/elasticsearch/package.json +++ b/packages/elasticsearch/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "dependencies": { "@elastic/elasticsearch": "9.3.4", diff --git a/packages/email-mock-receiver/package.json b/packages/email-mock-receiver/package.json index 1fa123d24..4e8f3bd14 100644 --- a/packages/email-mock-receiver/package.json +++ b/packages/email-mock-receiver/package.json @@ -20,10 +20,7 @@ "dev": "echo \"Implement me!\"", "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "test": "yarn test:unit", - "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "oxlint -c ../../.oxlintrc.json .", - "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", - "lint:eslint": "yarn lint:code && yarn lint:test" + "lint": "oxlint -c ../../.oxlintrc.json ." }, "devDependencies": { "sinon": "21.0.3" diff --git a/packages/errors/package.json b/packages/errors/package.json index 2775196e6..f25c2bf7a 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -32,8 +32,7 @@ "build:types": "tsc --emitDeclarationOnly --declaration --declarationMap --outDir types", "test": "NODE_ENV=testing vitest run --coverage", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "dependencies": {}, "devDependencies": { diff --git a/packages/express-test/package.json b/packages/express-test/package.json index e4f773330..01ea1313c 100644 --- a/packages/express-test/package.json +++ b/packages/express-test/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "dependencies": { "@tryghost/jest-snapshot": "^2.0.3", diff --git a/packages/http-cache-utils/package.json b/packages/http-cache-utils/package.json index 508e0bc00..4f5335ae2 100644 --- a/packages/http-cache-utils/package.json +++ b/packages/http-cache-utils/package.json @@ -20,10 +20,7 @@ "dev": "echo \"Implement me!\"", "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "test": "yarn test:unit", - "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "oxlint -c ../../.oxlintrc.json .", - "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", - "lint:eslint": "yarn lint:code && yarn lint:test" + "lint": "oxlint -c ../../.oxlintrc.json ." }, "devDependencies": { "sinon": "21.0.3" diff --git a/packages/http-stream/package.json b/packages/http-stream/package.json index e02417676..da37e828e 100644 --- a/packages/http-stream/package.json +++ b/packages/http-stream/package.json @@ -19,11 +19,8 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint:code": "eslint *.js lib/ --ext .js --cache", "lint": "oxlint -c ../../.oxlintrc.json .", - "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", - "posttest": "yarn lint", - "lint:eslint": "yarn lint:code && yarn lint:test" + "posttest": "yarn lint" }, "dependencies": { "@tryghost/errors": "^3.0.3", diff --git a/packages/jest-snapshot/package.json b/packages/jest-snapshot/package.json index 5c78ca2d4..dd69fb767 100644 --- a/packages/jest-snapshot/package.json +++ b/packages/jest-snapshot/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "dependencies": { "@jest/expect": "30.3.0", diff --git a/packages/job-manager/package.json b/packages/job-manager/package.json index 0be616b90..f54d84b5d 100644 --- a/packages/job-manager/package.json +++ b/packages/job-manager/package.json @@ -20,10 +20,7 @@ "dev": "echo \"Implement me!\"", "test:unit": "NODE_ENV=testing vitest run --coverage", "test": "yarn test:unit", - "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "oxlint -c ../../.oxlintrc.json .", - "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", - "lint:eslint": "yarn lint:code && yarn lint:test" + "lint": "oxlint -c ../../.oxlintrc.json ." }, "dependencies": { "@breejs/later": "4.2.0", diff --git a/packages/logging/package.json b/packages/logging/package.json index 7dcff8e7d..3fa01cc0c 100644 --- a/packages/logging/package.json +++ b/packages/logging/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "dependencies": { "@tryghost/bunyan-rotating-filestream": "0.0.7", diff --git a/packages/metrics/package.json b/packages/metrics/package.json index ac8733b0a..4f5eb3276 100644 --- a/packages/metrics/package.json +++ b/packages/metrics/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "dependencies": { "@tryghost/elasticsearch": "^5.0.3", diff --git a/packages/mw-error-handler/package.json b/packages/mw-error-handler/package.json index 90fa778d6..7c93a4e02 100644 --- a/packages/mw-error-handler/package.json +++ b/packages/mw-error-handler/package.json @@ -21,10 +21,7 @@ "dev": "echo \"Implement me!\"", "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "test": "yarn test:unit", - "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "oxlint -c ../../.oxlintrc.json .", - "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", - "lint:eslint": "yarn lint:code && yarn lint:test" + "lint": "oxlint -c ../../.oxlintrc.json ." }, "dependencies": { "@tryghost/debug": "^2.0.3", diff --git a/packages/mw-vhost/package.json b/packages/mw-vhost/package.json index 796fd2cfd..b8ed4d9e4 100644 --- a/packages/mw-vhost/package.json +++ b/packages/mw-vhost/package.json @@ -20,10 +20,7 @@ "dev": "echo \"Implement me!\"", "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "test": "yarn test:unit", - "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "oxlint -c ../../.oxlintrc.json .", - "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", - "lint:eslint": "yarn lint:code && yarn lint:test" + "lint": "oxlint -c ../../.oxlintrc.json ." }, "devDependencies": { "supertest": "7.2.2" diff --git a/packages/nodemailer/package.json b/packages/nodemailer/package.json index 5b43da6b8..fd6aa68f9 100644 --- a/packages/nodemailer/package.json +++ b/packages/nodemailer/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "dependencies": { "@aws-sdk/client-sesv2": "3.1014.0", diff --git a/packages/pretty-cli/package.json b/packages/pretty-cli/package.json index 8e7489ba2..0ed93c280 100644 --- a/packages/pretty-cli/package.json +++ b/packages/pretty-cli/package.json @@ -21,8 +21,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "dependencies": { "chalk": "5.6.2", diff --git a/packages/pretty-stream/package.json b/packages/pretty-stream/package.json index 53a241efd..f7dc306fc 100644 --- a/packages/pretty-stream/package.json +++ b/packages/pretty-stream/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "dependencies": { "date-format": "4.0.14", diff --git a/packages/prometheus-metrics/package.json b/packages/prometheus-metrics/package.json index 1d5246819..45233405f 100644 --- a/packages/prometheus-metrics/package.json +++ b/packages/prometheus-metrics/package.json @@ -23,10 +23,7 @@ "test:unit": "NODE_ENV=testing vitest run --coverage", "test": "yarn test:types && yarn test:unit", "test:types": "tsc --noEmit", - "lint:code": "eslint src/ --ext .ts --cache", - "lint": "oxlint -c ../../.oxlintrc.json .", - "lint:test": "eslint -c test/.eslintrc.js test/ --ext .ts --cache", - "lint:eslint": "yarn lint:code && yarn lint:test" + "lint": "oxlint -c ../../.oxlintrc.json ." }, "dependencies": { "@tryghost/logging": "^4.0.3", diff --git a/packages/promise/package.json b/packages/promise/package.json index cad289f16..6270a25c8 100644 --- a/packages/promise/package.json +++ b/packages/promise/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "devDependencies": { "sinon": "21.0.3" diff --git a/packages/request/package.json b/packages/request/package.json index b6945f78e..8a2cd3733 100644 --- a/packages/request/package.json +++ b/packages/request/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "dependencies": { "@tryghost/errors": "^3.0.3", diff --git a/packages/root-utils/package.json b/packages/root-utils/package.json index f2d56a10a..daf0d7243 100644 --- a/packages/root-utils/package.json +++ b/packages/root-utils/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "dependencies": { "caller": "1.1.0", diff --git a/packages/security/package.json b/packages/security/package.json index 023d6b454..417ccada0 100644 --- a/packages/security/package.json +++ b/packages/security/package.json @@ -21,10 +21,7 @@ "dev": "echo \"Implement me!\"", "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "test": "yarn test:unit", - "lint:code": "eslint *.js lib/ --ext .js --cache", - "lint": "oxlint -c ../../.oxlintrc.json .", - "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", - "lint:eslint": "yarn lint:code && yarn lint:test" + "lint": "oxlint -c ../../.oxlintrc.json ." }, "dependencies": { "@tryghost/string": "0.3.1", diff --git a/packages/server/package.json b/packages/server/package.json index 8b4a764c9..66737a806 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "dependencies": { "@tryghost/debug": "^2.0.3", diff --git a/packages/tpl/package.json b/packages/tpl/package.json index e4a22b074..a0b14c887 100644 --- a/packages/tpl/package.json +++ b/packages/tpl/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "dependencies": {}, "devDependencies": { diff --git a/packages/validator/package.json b/packages/validator/package.json index cca0f33b9..a06acacbf 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "dependencies": { "@tryghost/errors": "^3.0.3", diff --git a/packages/version/package.json b/packages/version/package.json index c16298244..b4e0eef54 100644 --- a/packages/version/package.json +++ b/packages/version/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "dependencies": { "@tryghost/root-utils": "^2.0.3", diff --git a/packages/webhook-mock-receiver/package.json b/packages/webhook-mock-receiver/package.json index 3fd88c448..77a2d177e 100644 --- a/packages/webhook-mock-receiver/package.json +++ b/packages/webhook-mock-receiver/package.json @@ -19,11 +19,8 @@ "scripts": { "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "lint:code": "eslint *.js lib/ --ext .js --cache", "lint": "oxlint -c ../../.oxlintrc.json .", - "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", - "posttest": "yarn lint", - "lint:eslint": "yarn lint:code && yarn lint:test" + "posttest": "yarn lint" }, "dependencies": { "p-wait-for": "6.0.0" diff --git a/packages/zip/package.json b/packages/zip/package.json index 55044aa56..dacddb8ca 100644 --- a/packages/zip/package.json +++ b/packages/zip/package.json @@ -20,8 +20,7 @@ "dev": "echo \"Implement me!\"", "test": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "yarn lint", - "lint:eslint": "eslint . --ext .js --cache" + "posttest": "yarn lint" }, "dependencies": { "@tryghost/errors": "^3.0.3", diff --git a/scripts/lint-custom-rules.js b/scripts/lint-custom-rules.js new file mode 100644 index 000000000..63804d88c --- /dev/null +++ b/scripts/lint-custom-rules.js @@ -0,0 +1,267 @@ +#!/usr/bin/env node +'use strict'; + +const fs = require('fs'); +const {spawnSync} = require('child_process'); + +const RULES = { + noNativeError: { + key: 'no-native-error', + aliases: [ + 'ghost/ghost-custom/no-native-error', + 'custom/no-native-error', + 'no-native-error' + ] + }, + ghostErrorUsage: { + key: 'ghost-error-usage', + aliases: [ + 'ghost/ghost-custom/ghost-error-usage', + 'custom/ghost-error-usage', + 'ghost-error-usage' + ] + }, + nodeAssertStrict: { + key: 'node-assert-strict', + aliases: [ + 'ghost/ghost-custom/node-assert-strict', + 'custom/node-assert-strict', + 'node-assert-strict' + ] + } +}; + +const ALL_RULE_KEYS = Object.keys(RULES); + +function runGitLsFiles() { + const result = spawnSync('git', ['ls-files', 'packages'], {encoding: 'utf8'}); + if (result.status !== 0) { + console.error(result.stderr || 'Failed to list files with git ls-files.'); + process.exit(result.status || 1); + } + + return result.stdout + .split('\n') + .map(file => file.trim()) + .filter(Boolean) + .filter(file => file.endsWith('.js') || file.endsWith('.ts')); +} + +function shouldIgnoreFile(file) { + return ( + file.includes('/node_modules/') || + file.includes('/build/') || + file.includes('/coverage/') || + file.includes('/cjs/') || + file.includes('/es/') || + file.includes('/types/') || + file.includes('/test/fixtures/') || + file.endsWith('.d.ts') + ); +} + +function isTestFile(file) { + return file.includes('/test/') || /\.test\.[jt]s$/.test(file); +} + +function findRuleKeysInDirective(line) { + const matchedRuleKeys = []; + + for (const ruleKey of ALL_RULE_KEYS) { + if (RULES[ruleKey].aliases.some(alias => line.includes(alias))) { + matchedRuleKeys.push(ruleKey); + } + } + + if (matchedRuleKeys.length > 0) { + return matchedRuleKeys; + } + + // Generic "eslint-disable" without specific rules should disable all custom checks. + if (line.includes('eslint-disable') || line.includes('lint-custom-disable')) { + return ALL_RULE_KEYS.slice(); + } + + return []; +} + +function buildDisableMap(lines) { + const fileDisabled = {}; + const lineDisabled = {}; + + for (const ruleKey of ALL_RULE_KEYS) { + fileDisabled[ruleKey] = false; + lineDisabled[ruleKey] = new Set(); + } + + lines.forEach((line, index) => { + if (!line.includes('disable')) { + return; + } + + const rules = findRuleKeysInDirective(line); + if (rules.length === 0) { + return; + } + + const isNextLineDirective = line.includes('eslint-disable-next-line') || line.includes('lint-custom-disable-next-line'); + const isLineDirective = line.includes('eslint-disable-line') || line.includes('lint-custom-disable-line'); + + for (const ruleKey of rules) { + if (isNextLineDirective) { + lineDisabled[ruleKey].add(index + 2); + } else if (isLineDirective) { + lineDisabled[ruleKey].add(index + 1); + } else if (line.includes('eslint-disable') || line.includes('lint-custom-disable')) { + fileDisabled[ruleKey] = true; + } + } + }); + + return {fileDisabled, lineDisabled}; +} + +function isRuleDisabled(ruleKey, lineNumber, disables) { + return disables.fileDisabled[ruleKey] || disables.lineDisabled[ruleKey].has(lineNumber); +} + +function firstMeaningfulCharAfterOpenParen(lines, startLine, afterOpenParen) { + const immediate = afterOpenParen.trim(); + if (immediate.length > 0) { + return immediate[0]; + } + + for (let i = startLine; i < lines.length; i += 1) { + const trimmed = lines[i].trim(); + if (trimmed.length === 0) { + continue; + } + if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) { + continue; + } + return trimmed[0]; + } + + return ''; +} + +function addIssue(issues, file, lineNumber, rule, message) { + issues.push({file, lineNumber, rule, message}); +} + +function checkFile(file, issues) { + const code = fs.readFileSync(file, 'utf8'); + const lines = code.split('\n'); + const disables = buildDisableMap(lines); + const testFile = isTestFile(file); + const jsSourceFile = file.endsWith('.js') && !testFile; + + lines.forEach((line, index) => { + const lineNumber = index + 1; + const trimmed = line.trim(); + + // Skip pure comment lines for line-based pattern checks. + if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) { + return; + } + + if (jsSourceFile) { + if (!isRuleDisabled('noNativeError', lineNumber, disables)) { + const hasNewError = /\bnew\s+Error\s*\(/.test(line); + if (hasNewError) { + addIssue( + issues, + file, + lineNumber, + RULES.noNativeError.key, + 'Use @tryghost/errors instead of new Error().' + ); + } + } + + if (!isRuleDisabled('ghostErrorUsage', lineNumber, disables)) { + const patterns = [ + /\bnew\s+errors\.[A-Za-z_$][\w$]*\s*\((.*)$/g, + /\bnew\s+([A-Z][a-zA-Z]+Error)\s*\((.*)$/g + ]; + + for (const pattern of patterns) { + let match; + while ((match = pattern.exec(line)) !== null) { + const className = match[1]; + if (typeof className === 'string' && !className.endsWith('Error')) { + continue; + } + + const argFirstChar = firstMeaningfulCharAfterOpenParen(lines, index + 1, match[2] || ''); + if (argFirstChar !== '' && argFirstChar !== '{') { + addIssue( + issues, + file, + lineNumber, + RULES.ghostErrorUsage.key, + 'Error constructors should receive an object argument.' + ); + } + } + } + } + } + + if (testFile && !isRuleDisabled('nodeAssertStrict', lineNumber, disables)) { + const importAssert = /^\s*import\s+.+\s+from\s+['"]assert['"]/.test(line) || /^\s*import\s+['"]assert['"]/.test(line); + const requireAssert = /\b(?:const|let|var)\s+[^=]+=\s*require\(\s*['"]assert['"]\s*\)/.test(line); + const strictMethod = /\bassert\.(strictEqual|deepStrictEqual|notStrictEqual|notDeepStrictEqual)\s*\(/.test(line); + + if (importAssert || requireAssert) { + addIssue( + issues, + file, + lineNumber, + RULES.nodeAssertStrict.key, + 'Use assert/strict (or node:assert/strict) instead of assert.' + ); + } + + if (strictMethod) { + addIssue( + issues, + file, + lineNumber, + RULES.nodeAssertStrict.key, + 'When using assert/strict, avoid strict* methods (use equal/deepEqual variants).' + ); + } + } + }); +} + +function main() { + const files = runGitLsFiles().filter(file => !shouldIgnoreFile(file)); + const issues = []; + + for (const file of files) { + checkFile(file, issues); + } + + issues.sort((a, b) => { + if (a.file !== b.file) { + return a.file.localeCompare(b.file); + } + return a.lineNumber - b.lineNumber; + }); + + if (issues.length === 0) { + console.log('Custom lint checks passed.'); + process.exit(0); + } + + for (const issue of issues) { + console.error(`${issue.file}:${issue.lineNumber} [${issue.rule}] ${issue.message}`); + } + + console.error(`\nCustom lint checks failed with ${issues.length} issue(s).`); + process.exit(1); +} + +main(); From 1fb071fd5423fc47afee1551d5a9eba74f72d85a Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Tue, 24 Mar 2026 07:27:04 -0500 Subject: [PATCH 5/5] chore: remove legacy eslint configs and comments --- packages/.eslintrc.js | 5 ----- packages/api-framework/test/.eslintrc.js | 4 ---- packages/bookshelf-collision/test/.eslintrc.js | 4 ---- packages/bookshelf-custom-query/test/.eslintrc.js | 4 ---- packages/bookshelf-eager-load/test/.eslintrc.js | 4 ---- packages/bookshelf-filter/test/.eslintrc.js | 4 ---- packages/bookshelf-has-posts/test/.eslintrc.js | 4 ---- packages/bookshelf-include-count/test/.eslintrc.js | 4 ---- packages/bookshelf-order/test/.eslintrc.js | 4 ---- packages/bookshelf-pagination/test/.eslintrc.js | 4 ---- packages/bookshelf-plugins/test/.eslintrc.js | 4 ---- packages/bookshelf-search/test/.eslintrc.js | 4 ---- .../bookshelf-transaction-events/test/.eslintrc.js | 4 ---- packages/config/test/.eslintrc.js | 4 ---- packages/database-info/test/.eslintrc.js | 4 ---- packages/debug/test/.eslintrc.js | 4 ---- packages/domain-events/test/.eslintrc.js | 4 ---- packages/elasticsearch/test/.eslintrc.js | 4 ---- packages/email-mock-receiver/test/.eslintrc.js | 7 ------- packages/errors/.eslintignore | 4 ---- packages/errors/.eslintrc.js | 4 ---- packages/errors/src/GhostError.ts | 6 ------ packages/errors/src/utils.ts | 3 --- packages/errors/test/.eslintrc.js | 4 ---- packages/express-test/lib/Agent.js | 14 ++++---------- packages/express-test/lib/ExpectRequest.js | 2 +- packages/express-test/test/.eslintrc.js | 7 ------- packages/express-test/test/utils/overrides.js | 3 --- packages/http-cache-utils/test/.eslintrc.js | 4 ---- packages/http-stream/test/.eslintrc.js | 4 ---- packages/jest-snapshot/.eslintrc.js | 5 ----- packages/jest-snapshot/test/.eslintrc.js | 4 ---- packages/job-manager/test/.eslintrc.js | 4 ---- .../job-manager/test/examples/graceful-shutdown.js | 3 +-- packages/job-manager/test/jobs/graceful.js | 2 +- packages/logging/lib/GhostLogger.js | 3 ++- packages/logging/test/.eslintrc.js | 4 ---- packages/metrics/lib/GhostMetrics.js | 3 ++- packages/metrics/test/.eslintrc.js | 4 ---- packages/mw-error-handler/lib/mw-error-handler.js | 2 -- packages/mw-error-handler/test/.eslintrc.js | 4 ---- packages/mw-vhost/README.md | 2 -- packages/mw-vhost/lib/vhost.js | 7 ++++--- packages/mw-vhost/test/.eslintrc.js | 4 ---- packages/nodemailer/lib/nodemailer.js | 2 -- packages/nodemailer/test/.eslintrc.js | 4 ---- packages/pretty-cli/lib/ui.js | 3 ++- packages/pretty-cli/test/.eslintrc.js | 4 ---- packages/pretty-stream/lib/PrettyStream.js | 2 -- packages/pretty-stream/test/.eslintrc.js | 4 ---- packages/prometheus-metrics/.eslintrc.js | 5 ----- packages/prometheus-metrics/test/.eslintrc.js | 8 -------- packages/promise/lib/pool.js | 4 ++-- packages/promise/test/.eslintrc.js | 4 ---- packages/request/test/.eslintrc.js | 4 ---- packages/root-utils/test/.eslintrc.js | 4 ---- packages/security/test/.eslintrc.js | 4 ---- packages/server/test/.eslintrc.js | 4 ---- packages/tpl/lib/tpl.js | 2 +- packages/tpl/test/.eslintrc.js | 4 ---- packages/validator/lib/is-byte-length.js | 1 - packages/validator/lib/is-email.js | 5 ----- packages/validator/lib/is-fqdn.js | 1 - packages/validator/test/.eslintrc.js | 4 ---- packages/version/test/.eslintrc.js | 4 ---- packages/webhook-mock-receiver/test/.eslintrc.js | 7 ------- packages/zip/test/.eslintrc.js | 8 -------- 67 files changed, 20 insertions(+), 258 deletions(-) delete mode 100644 packages/.eslintrc.js delete mode 100644 packages/api-framework/test/.eslintrc.js delete mode 100644 packages/bookshelf-collision/test/.eslintrc.js delete mode 100644 packages/bookshelf-custom-query/test/.eslintrc.js delete mode 100644 packages/bookshelf-eager-load/test/.eslintrc.js delete mode 100644 packages/bookshelf-filter/test/.eslintrc.js delete mode 100644 packages/bookshelf-has-posts/test/.eslintrc.js delete mode 100644 packages/bookshelf-include-count/test/.eslintrc.js delete mode 100644 packages/bookshelf-order/test/.eslintrc.js delete mode 100644 packages/bookshelf-pagination/test/.eslintrc.js delete mode 100644 packages/bookshelf-plugins/test/.eslintrc.js delete mode 100644 packages/bookshelf-search/test/.eslintrc.js delete mode 100644 packages/bookshelf-transaction-events/test/.eslintrc.js delete mode 100644 packages/config/test/.eslintrc.js delete mode 100644 packages/database-info/test/.eslintrc.js delete mode 100644 packages/debug/test/.eslintrc.js delete mode 100644 packages/domain-events/test/.eslintrc.js delete mode 100644 packages/elasticsearch/test/.eslintrc.js delete mode 100644 packages/email-mock-receiver/test/.eslintrc.js delete mode 100644 packages/errors/.eslintignore delete mode 100644 packages/errors/.eslintrc.js delete mode 100644 packages/errors/test/.eslintrc.js delete mode 100644 packages/express-test/test/.eslintrc.js delete mode 100644 packages/http-cache-utils/test/.eslintrc.js delete mode 100644 packages/http-stream/test/.eslintrc.js delete mode 100644 packages/jest-snapshot/.eslintrc.js delete mode 100644 packages/jest-snapshot/test/.eslintrc.js delete mode 100644 packages/job-manager/test/.eslintrc.js delete mode 100644 packages/logging/test/.eslintrc.js delete mode 100644 packages/metrics/test/.eslintrc.js delete mode 100644 packages/mw-error-handler/test/.eslintrc.js delete mode 100644 packages/mw-vhost/test/.eslintrc.js delete mode 100644 packages/nodemailer/test/.eslintrc.js delete mode 100644 packages/pretty-cli/test/.eslintrc.js delete mode 100644 packages/pretty-stream/test/.eslintrc.js delete mode 100644 packages/prometheus-metrics/.eslintrc.js delete mode 100644 packages/prometheus-metrics/test/.eslintrc.js delete mode 100644 packages/promise/test/.eslintrc.js delete mode 100644 packages/request/test/.eslintrc.js delete mode 100644 packages/root-utils/test/.eslintrc.js delete mode 100644 packages/security/test/.eslintrc.js delete mode 100644 packages/server/test/.eslintrc.js delete mode 100644 packages/tpl/test/.eslintrc.js delete mode 100644 packages/validator/test/.eslintrc.js delete mode 100644 packages/version/test/.eslintrc.js delete mode 100644 packages/webhook-mock-receiver/test/.eslintrc.js delete mode 100644 packages/zip/test/.eslintrc.js diff --git a/packages/.eslintrc.js b/packages/.eslintrc.js deleted file mode 100644 index a0af5f89d..000000000 --- a/packages/.eslintrc.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/node"], - ignorePatterns: ["**/build/**"], -}; diff --git a/packages/api-framework/test/.eslintrc.js b/packages/api-framework/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/api-framework/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/bookshelf-collision/test/.eslintrc.js b/packages/bookshelf-collision/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/bookshelf-collision/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/bookshelf-custom-query/test/.eslintrc.js b/packages/bookshelf-custom-query/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/bookshelf-custom-query/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/bookshelf-eager-load/test/.eslintrc.js b/packages/bookshelf-eager-load/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/bookshelf-eager-load/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/bookshelf-filter/test/.eslintrc.js b/packages/bookshelf-filter/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/bookshelf-filter/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/bookshelf-has-posts/test/.eslintrc.js b/packages/bookshelf-has-posts/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/bookshelf-has-posts/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/bookshelf-include-count/test/.eslintrc.js b/packages/bookshelf-include-count/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/bookshelf-include-count/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/bookshelf-order/test/.eslintrc.js b/packages/bookshelf-order/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/bookshelf-order/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/bookshelf-pagination/test/.eslintrc.js b/packages/bookshelf-pagination/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/bookshelf-pagination/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/bookshelf-plugins/test/.eslintrc.js b/packages/bookshelf-plugins/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/bookshelf-plugins/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/bookshelf-search/test/.eslintrc.js b/packages/bookshelf-search/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/bookshelf-search/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/bookshelf-transaction-events/test/.eslintrc.js b/packages/bookshelf-transaction-events/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/bookshelf-transaction-events/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/config/test/.eslintrc.js b/packages/config/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/config/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/database-info/test/.eslintrc.js b/packages/database-info/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/database-info/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/debug/test/.eslintrc.js b/packages/debug/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/debug/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/domain-events/test/.eslintrc.js b/packages/domain-events/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/domain-events/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/elasticsearch/test/.eslintrc.js b/packages/elasticsearch/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/elasticsearch/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/email-mock-receiver/test/.eslintrc.js b/packages/email-mock-receiver/test/.eslintrc.js deleted file mode 100644 index c9b01755e..000000000 --- a/packages/email-mock-receiver/test/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], - globals: { - beforeAll: "readonly", - }, -}; diff --git a/packages/errors/.eslintignore b/packages/errors/.eslintignore deleted file mode 100644 index f777d3abe..000000000 --- a/packages/errors/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -coverage -cjs -es -types diff --git a/packages/errors/.eslintrc.js b/packages/errors/.eslintrc.js deleted file mode 100644 index 1c61e22e1..000000000 --- a/packages/errors/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/ts"], -}; diff --git a/packages/errors/src/GhostError.ts b/packages/errors/src/GhostError.ts index 9e009f1c4..51db1116f 100644 --- a/packages/errors/src/GhostError.ts +++ b/packages/errors/src/GhostError.ts @@ -9,7 +9,6 @@ export interface GhostErrorOptions { context?: string; help?: string; errorType?: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any errorDetails?: any; code?: string; property?: string; @@ -25,7 +24,6 @@ export class GhostError extends Error { id: string; context?: string; help?: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any errorDetails: any; code: string | null; property: string | null; @@ -66,9 +64,7 @@ export class GhostError extends Error { if (options.err) { // CASE: Support err as string (it happens that third party libs return a string instead of an error instance) if (typeof options.err === "string") { - /* eslint-disable no-restricted-syntax */ options.err = new Error(options.err); - /* eslint-enable no-restricted-syntax */ } Object.getOwnPropertyNames(options.err).forEach((property) => { @@ -80,7 +76,6 @@ export class GhostError extends Error { // CASE: `code` should put options as priority over err if (property === "code") { - // eslint-disable-next-line @typescript-eslint/no-explicit-any this[property] = this[property] || (options.err as any)[property]; return; } @@ -90,7 +85,6 @@ export class GhostError extends Error { return; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any (this as any)[property] = (options.err as any)[property] || (this as any)[property]; }); } diff --git a/packages/errors/src/utils.ts b/packages/errors/src/utils.ts index 8ed4a07a8..eed5e8442 100644 --- a/packages/errors/src/utils.ts +++ b/packages/errors/src/utils.ts @@ -1,12 +1,10 @@ import { GhostError } from "./GhostError"; import * as errors from "./errors"; -// eslint-disable-next-line @typescript-eslint/no-explicit-any type AnyObject = Record; // structuredClone doesn't preserve custom properties on Error subclasses // (see https://github.com/ungap/structured-clone/issues/12) -// eslint-disable-next-line @typescript-eslint/no-explicit-any function deepCloneValue(value: any): any { if (value === null || typeof value !== "object") { return value; @@ -82,7 +80,6 @@ const _private = { default: "server_error", }; - // eslint-disable-next-line @typescript-eslint/no-unused-vars const { detail, code, ...properties } = _private.serialize(err); return { diff --git a/packages/errors/test/.eslintrc.js b/packages/errors/test/.eslintrc.js deleted file mode 100644 index a10d22c75..000000000 --- a/packages/errors/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/ts-test"], -}; diff --git a/packages/express-test/lib/Agent.js b/packages/express-test/lib/Agent.js index a0679cba0..3da6770db 100644 --- a/packages/express-test/lib/Agent.js +++ b/packages/express-test/lib/Agent.js @@ -1,4 +1,4 @@ -/* eslint-disable ghost/ghost-custom/no-native-error */ +/* lint-custom-disable no-native-error */ const { CookieJar } = require("cookiejar"); const ExpectRequest = require("./ExpectRequest"); const { RequestOptions } = require("./Request"); @@ -73,15 +73,11 @@ class Agent { _mergeOptions(method, url, options = {}) { // It doesn't make sense to call this method without these properties if (!method) { - throw new Error( - "_mergeOptions cannot be called without a method", - ); /* eslint-disable-line no-restricted-syntax */ + throw new Error("_mergeOptions cannot be called without a method"); } if (!url) { - throw new Error( - "_mergeOptions cannot be called without a url", - ); /* eslint-disable-line no-restricted-syntax */ + throw new Error("_mergeOptions cannot be called without a url"); } // urlOptions @@ -109,9 +105,7 @@ class Agent { ["get", "post", "put", "patch", "delete", "options", "head"].forEach((method) => { Agent.prototype[method] = function (url, options) { if (!url) { - throw new Error( - "Cannot make a request without supplying a url", - ); /* eslint-disable-line no-restricted-syntax */ + throw new Error("Cannot make a request without supplying a url"); } return new ExpectRequest( this.app, diff --git a/packages/express-test/lib/ExpectRequest.js b/packages/express-test/lib/ExpectRequest.js index 30a910beb..88cd9363c 100644 --- a/packages/express-test/lib/ExpectRequest.js +++ b/packages/express-test/lib/ExpectRequest.js @@ -30,7 +30,7 @@ class ExpectRequest extends Request { expect(callback) { if (typeof callback !== "function") { - // eslint-disable-next-line ghost/ghost-custom/no-native-error + // lint-custom-disable-next-line no-native-error throw new Error( "express-test expect() requires a callback function, did you mean expectStatus or expectHeader?", ); diff --git a/packages/express-test/test/.eslintrc.js b/packages/express-test/test/.eslintrc.js deleted file mode 100644 index c9b01755e..000000000 --- a/packages/express-test/test/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], - globals: { - beforeAll: "readonly", - }, -}; diff --git a/packages/express-test/test/utils/overrides.js b/packages/express-test/test/utils/overrides.js index 27090e9c4..694d00c0f 100644 --- a/packages/express-test/test/utils/overrides.js +++ b/packages/express-test/test/utils/overrides.js @@ -1,13 +1,10 @@ const { snapshotManager } = require("@tryghost/jest-snapshot"); -/* eslint-disable ghost/mocha/no-mocha-arrows, ghost/mocha/no-top-level-hooks, ghost/mocha/handle-done-callback */ beforeAll(() => { - // eslint-disable-line no-undef snapshotManager.resetRegistry(); }); beforeEach((context) => { - // eslint-disable-line no-undef // Reconstruct full title similar to mocha's fullTitle() const parts = []; let suite = context.task.suite; diff --git a/packages/http-cache-utils/test/.eslintrc.js b/packages/http-cache-utils/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/http-cache-utils/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/http-stream/test/.eslintrc.js b/packages/http-stream/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/http-stream/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/jest-snapshot/.eslintrc.js b/packages/jest-snapshot/.eslintrc.js deleted file mode 100644 index 78b9d4c9b..000000000 --- a/packages/jest-snapshot/.eslintrc.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/node"], - ignorePatterns: ["coverage"], -}; diff --git a/packages/jest-snapshot/test/.eslintrc.js b/packages/jest-snapshot/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/jest-snapshot/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/job-manager/test/.eslintrc.js b/packages/job-manager/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/job-manager/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/job-manager/test/examples/graceful-shutdown.js b/packages/job-manager/test/examples/graceful-shutdown.js index 8ae1f7272..caf47a7b2 100644 --- a/packages/job-manager/test/examples/graceful-shutdown.js +++ b/packages/job-manager/test/examples/graceful-shutdown.js @@ -1,5 +1,4 @@ -/* eslint-disable no-console */ - +/* oxlint-disable no-console */ const path = require("path"); const setTimeoutPromise = require("util").promisify(setTimeout); const JobManager = require("../../lib/job-manager"); diff --git a/packages/job-manager/test/jobs/graceful.js b/packages/job-manager/test/jobs/graceful.js index 47d4fe920..340859b6d 100644 --- a/packages/job-manager/test/jobs/graceful.js +++ b/packages/job-manager/test/jobs/graceful.js @@ -1,4 +1,4 @@ -/* eslint-disable no-console */ +/* oxlint-disable no-console */ const setTimeoutPromise = require("util").promisify(setTimeout); const { isMainThread, parentPort } = require("worker_threads"); diff --git a/packages/logging/lib/GhostLogger.js b/packages/logging/lib/GhostLogger.js index 5634393e8..89f325569 100644 --- a/packages/logging/lib/GhostLogger.js +++ b/packages/logging/lib/GhostLogger.js @@ -89,7 +89,8 @@ class GhostLogger { let transportFn = `set${upperFirst(transport)}Stream`; if (!this[transportFn]) { - throw new Error(`${upperFirst(transport)} is an invalid transport`); // eslint-disable-line + // lint-custom-disable-next-line no-native-error + throw new Error(`${upperFirst(transport)} is an invalid transport`); } this[transportFn](); diff --git a/packages/logging/test/.eslintrc.js b/packages/logging/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/logging/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/metrics/lib/GhostMetrics.js b/packages/metrics/lib/GhostMetrics.js index 893fe86f9..99dcf316b 100644 --- a/packages/metrics/lib/GhostMetrics.js +++ b/packages/metrics/lib/GhostMetrics.js @@ -39,7 +39,8 @@ class GhostMetrics { let transportFn = `setup${transport[0].toUpperCase()}${transport.substr(1)}Shipper`; if (!this[transportFn]) { - throw new Error(`${transport} is an invalid transport`); // eslint-disable-line + // lint-custom-disable-next-line no-native-error + throw new Error(`${transport} is an invalid transport`); } this[transportFn](); diff --git a/packages/metrics/test/.eslintrc.js b/packages/metrics/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/metrics/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/mw-error-handler/lib/mw-error-handler.js b/packages/mw-error-handler/lib/mw-error-handler.js index 1ff23dfaa..918360acf 100644 --- a/packages/mw-error-handler/lib/mw-error-handler.js +++ b/packages/mw-error-handler/lib/mw-error-handler.js @@ -136,7 +136,6 @@ module.exports.prepareError = function prepareError(err, req, res, next) { }; module.exports.prepareStack = function prepareStack(err, req, res, next) { - // eslint-disable-line no-unused-vars const clonedError = prepareStackForUser(err); next(clonedError); @@ -150,7 +149,6 @@ module.exports.prepareStack = function prepareStack(err, req, res, next) { * @param {import('express').NextFunction} next */ module.exports.jsonErrorRenderer = function jsonErrorRenderer(err, req, res, next) { - // eslint-disable-line no-unused-vars const userError = prepareUserMessage(err, req); res.json({ diff --git a/packages/mw-error-handler/test/.eslintrc.js b/packages/mw-error-handler/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/mw-error-handler/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/mw-vhost/README.md b/packages/mw-vhost/README.md index 3e55ec4f3..d4064cfa0 100644 --- a/packages/mw-vhost/README.md +++ b/packages/mw-vhost/README.md @@ -8,8 +8,6 @@ Forked from https://github.com/expressjs/vhost/ which appears abandoned. ## API - - ```js var vhost = require("vhost"); ``` diff --git a/packages/mw-vhost/lib/vhost.js b/packages/mw-vhost/lib/vhost.js index 6c43f4593..a437c6722 100644 --- a/packages/mw-vhost/lib/vhost.js +++ b/packages/mw-vhost/lib/vhost.js @@ -1,6 +1,4 @@ // This a fork of expressjs/vhost with trust proxy support -/* eslint-disable */ - /*! * vhost * Copyright(c) 2014 Jonathan Ong @@ -39,21 +37,24 @@ var ESCAPE_REPLACE = "\\$1"; function vhost(hostname, handle) { if (!hostname) { + // lint-custom-disable-next-line ghost-error-usage throw new TypeError("argument hostname is required"); } if (!handle) { + // lint-custom-disable-next-line ghost-error-usage throw new TypeError("argument handle is required"); } if (typeof handle !== "function") { + // lint-custom-disable-next-line ghost-error-usage throw new TypeError("argument handle must be a function"); } // create regular expression for hostname var regexp = hostregexp(hostname); - return function vhost(req, res, next) { + return function vhostMiddleware(req, res, next) { var vhostdata = vhostof(req, regexp); if (!vhostdata) { diff --git a/packages/mw-vhost/test/.eslintrc.js b/packages/mw-vhost/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/mw-vhost/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/nodemailer/lib/nodemailer.js b/packages/nodemailer/lib/nodemailer.js index 445ff30ef..7c3bbb3a9 100644 --- a/packages/nodemailer/lib/nodemailer.js +++ b/packages/nodemailer/lib/nodemailer.js @@ -1,5 +1,3 @@ -/* eslint-disable no-case-declarations */ - const errors = require("@tryghost/errors"); const nodemailer = require("nodemailer"); const tpl = require("@tryghost/tpl"); diff --git a/packages/nodemailer/test/.eslintrc.js b/packages/nodemailer/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/nodemailer/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/pretty-cli/lib/ui.js b/packages/pretty-cli/lib/ui.js index bdf16c48d..186ec2132 100644 --- a/packages/pretty-cli/lib/ui.js +++ b/packages/pretty-cli/lib/ui.js @@ -1,5 +1,6 @@ +/* oxlint-disable no-console */ const { default: chalk } = require("chalk"); -const log = (...args) => console.log(...args); // eslint-disable-line no-console +const log = (...args) => console.log(...args); module.exports.log = log; diff --git a/packages/pretty-cli/test/.eslintrc.js b/packages/pretty-cli/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/pretty-cli/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/pretty-stream/lib/PrettyStream.js b/packages/pretty-stream/lib/PrettyStream.js index 5f897a358..82ed83c30 100644 --- a/packages/pretty-stream/lib/PrettyStream.js +++ b/packages/pretty-stream/lib/PrettyStream.js @@ -62,7 +62,6 @@ function colorize(colors, value) { } function statusCode(status) { - /* eslint-disable indent */ const color = status >= 500 ? "red" @@ -73,7 +72,6 @@ function statusCode(status) { : status >= 200 ? "green" : "default"; // no color - /* eslint-enable indent */ return colorize(color, status); } diff --git a/packages/pretty-stream/test/.eslintrc.js b/packages/pretty-stream/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/pretty-stream/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/prometheus-metrics/.eslintrc.js b/packages/prometheus-metrics/.eslintrc.js deleted file mode 100644 index 0adb06575..000000000 --- a/packages/prometheus-metrics/.eslintrc.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - parser: "@typescript-eslint/parser", - plugins: ["ghost"], - extends: ["plugin:ghost/node"], -}; diff --git a/packages/prometheus-metrics/test/.eslintrc.js b/packages/prometheus-metrics/test/.eslintrc.js deleted file mode 100644 index 47a9e966e..000000000 --- a/packages/prometheus-metrics/test/.eslintrc.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - parser: "@typescript-eslint/parser", - plugins: ["ghost"], - extends: ["plugin:ghost/test"], - globals: { - afterAll: "readonly", - }, -}; diff --git a/packages/promise/lib/pool.js b/packages/promise/lib/pool.js index e2cfec798..805f8bef6 100644 --- a/packages/promise/lib/pool.js +++ b/packages/promise/lib/pool.js @@ -7,8 +7,8 @@ async function pool(tasks, maxConcurrent) { if (maxConcurrent < 1) { - // eslint-disable-next-line ghost/ghost-custom/no-native-error - throw new Error("Must set at least 1 concurrent workers"); // eslint-disable-line no-restricted-syntax + // lint-custom-disable-next-line no-native-error + throw new Error("Must set at least 1 concurrent workers"); } const taskIterator = tasks.entries(); diff --git a/packages/promise/test/.eslintrc.js b/packages/promise/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/promise/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/request/test/.eslintrc.js b/packages/request/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/request/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/root-utils/test/.eslintrc.js b/packages/root-utils/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/root-utils/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/security/test/.eslintrc.js b/packages/security/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/security/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/server/test/.eslintrc.js b/packages/server/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/server/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/tpl/lib/tpl.js b/packages/tpl/lib/tpl.js index b7381d4c6..72bffb1fc 100644 --- a/packages/tpl/lib/tpl.js +++ b/packages/tpl/lib/tpl.js @@ -23,7 +23,7 @@ module.exports = (string, data) => { processedString = processedString.replace(interpolate, (_match, key) => { const trimmed = key.trim(); if (!(trimmed in data)) { - // eslint-disable-next-line ghost/ghost-custom/ghost-error-usage + // lint-custom-disable-next-line ghost-error-usage throw new ReferenceError(`${trimmed} is not defined`); } return data[trimmed]; diff --git a/packages/tpl/test/.eslintrc.js b/packages/tpl/test/.eslintrc.js deleted file mode 100644 index 6282aa6f4..000000000 --- a/packages/tpl/test/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - plugins: ["ghost"], - extends: ["plugin:ghost/test"], -}; diff --git a/packages/validator/lib/is-byte-length.js b/packages/validator/lib/is-byte-length.js index ec6d00a5f..e23fb9c51 100644 --- a/packages/validator/lib/is-byte-length.js +++ b/packages/validator/lib/is-byte-length.js @@ -1,6 +1,5 @@ const assertString = require("./util/assert-string"); -/* eslint-disable prefer-rest-params */ module.exports = function isByteLength(str, options) { assertString(str); let min; diff --git a/packages/validator/lib/is-email.js b/packages/validator/lib/is-email.js index a5408856b..6ac7e2350 100644 --- a/packages/validator/lib/is-email.js +++ b/packages/validator/lib/is-email.js @@ -2,7 +2,6 @@ * This file is a copy of validator.js isEmail method - v13.7.0: * https://github.com/validatorjs/validator.js/blob/531dc7f1f75613bec75c6d888b46480455e78dc7/src/lib/isEmail.js */ -/* eslint-disable camelcase */ const assertString = require("./util/assert-string"); const merge = require("./util/merge"); const isByteLength = require("./is-byte-length"); @@ -20,8 +19,6 @@ const default_email_options = { host_whitelist: [], }; -/* eslint-disable max-len */ -/* eslint-disable no-control-regex */ const splitNameAddress = /^([^\x00-\x1F\x7F-\x9F\cX]+)