From 2c065dcc04d7b2b4e5e57e3b00700bf6cb7685b0 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Thu, 11 Aug 2022 16:39:37 +0200 Subject: [PATCH 001/118] Extracted shared API framework to separate package refs https://github.com/TryGhost/Toolbox/issues/363 - this API framework is standalone and should be pulled out into a separate package so we can define its boundaries more clearly, and promote better testing of smaller parts --- ghost/api-framework/.eslintrc.js | 6 + ghost/api-framework/README.md | 23 + ghost/api-framework/index.js | 1 + ghost/api-framework/lib/api-framework.js | 29 ++ ghost/api-framework/lib/frame.js | 95 ++++ ghost/api-framework/lib/headers.js | 152 +++++++ ghost/api-framework/lib/http.js | 127 ++++++ ghost/api-framework/lib/pipeline.js | 262 +++++++++++ ghost/api-framework/lib/serializers/handle.js | 140 ++++++ ghost/api-framework/lib/serializers/index.js | 13 + .../lib/serializers/input/all.js | 41 ++ .../lib/serializers/input/index.js | 5 + .../lib/serializers/output/index.js | 1 + ghost/api-framework/lib/utils/index.js | 5 + ghost/api-framework/lib/utils/options.js | 23 + ghost/api-framework/lib/validators/handle.js | 68 +++ ghost/api-framework/lib/validators/index.js | 9 + .../api-framework/lib/validators/input/all.js | 213 +++++++++ .../lib/validators/input/index.js | 5 + ghost/api-framework/package.json | 34 ++ ghost/api-framework/test/.eslintrc.js | 6 + ghost/api-framework/test/frame.test.js | 101 +++++ ghost/api-framework/test/headers.test.js | 142 ++++++ ghost/api-framework/test/http.test.js | 90 ++++ ghost/api-framework/test/pipeline.test.js | 253 +++++++++++ .../test/serializers/handle.test.js | 412 ++++++++++++++++++ .../test/serializers/input/all.test.js | 81 ++++ ghost/api-framework/test/util/options.test.js | 23 + .../test/validators/handle.test.js | 60 +++ .../test/validators/input/all.test.js | 405 +++++++++++++++++ 30 files changed, 2825 insertions(+) create mode 100644 ghost/api-framework/.eslintrc.js create mode 100644 ghost/api-framework/README.md create mode 100644 ghost/api-framework/index.js create mode 100644 ghost/api-framework/lib/api-framework.js create mode 100644 ghost/api-framework/lib/frame.js create mode 100644 ghost/api-framework/lib/headers.js create mode 100644 ghost/api-framework/lib/http.js create mode 100644 ghost/api-framework/lib/pipeline.js create mode 100644 ghost/api-framework/lib/serializers/handle.js create mode 100644 ghost/api-framework/lib/serializers/index.js create mode 100644 ghost/api-framework/lib/serializers/input/all.js create mode 100644 ghost/api-framework/lib/serializers/input/index.js create mode 100644 ghost/api-framework/lib/serializers/output/index.js create mode 100644 ghost/api-framework/lib/utils/index.js create mode 100644 ghost/api-framework/lib/utils/options.js create mode 100644 ghost/api-framework/lib/validators/handle.js create mode 100644 ghost/api-framework/lib/validators/index.js create mode 100644 ghost/api-framework/lib/validators/input/all.js create mode 100644 ghost/api-framework/lib/validators/input/index.js create mode 100644 ghost/api-framework/package.json create mode 100644 ghost/api-framework/test/.eslintrc.js create mode 100644 ghost/api-framework/test/frame.test.js create mode 100644 ghost/api-framework/test/headers.test.js create mode 100644 ghost/api-framework/test/http.test.js create mode 100644 ghost/api-framework/test/pipeline.test.js create mode 100644 ghost/api-framework/test/serializers/handle.test.js create mode 100644 ghost/api-framework/test/serializers/input/all.test.js create mode 100644 ghost/api-framework/test/util/options.test.js create mode 100644 ghost/api-framework/test/validators/handle.test.js create mode 100644 ghost/api-framework/test/validators/input/all.test.js diff --git a/ghost/api-framework/.eslintrc.js b/ghost/api-framework/.eslintrc.js new file mode 100644 index 00000000000..c9c1bcb5226 --- /dev/null +++ b/ghost/api-framework/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: ['ghost'], + extends: [ + 'plugin:ghost/node' + ] +}; diff --git a/ghost/api-framework/README.md b/ghost/api-framework/README.md new file mode 100644 index 00000000000..64191b307a0 --- /dev/null +++ b/ghost/api-framework/README.md @@ -0,0 +1,23 @@ +# Api Framework + +API framework used by Ghost + + +## 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/ghost/api-framework/index.js b/ghost/api-framework/index.js new file mode 100644 index 00000000000..373c844ba24 --- /dev/null +++ b/ghost/api-framework/index.js @@ -0,0 +1 @@ +module.exports = require('./lib/api-framework'); diff --git a/ghost/api-framework/lib/api-framework.js b/ghost/api-framework/lib/api-framework.js new file mode 100644 index 00000000000..fdda5f7d74c --- /dev/null +++ b/ghost/api-framework/lib/api-framework.js @@ -0,0 +1,29 @@ +module.exports = { + get headers() { + return require('./headers'); + }, + + get http() { + return require('./http'); + }, + + get Frame() { + return require('./frame'); + }, + + get pipeline() { + return require('./pipeline'); + }, + + get validators() { + return require('./validators'); + }, + + get serializers() { + return require('./serializers'); + }, + + get utils() { + return require('./utils'); + } +}; diff --git a/ghost/api-framework/lib/frame.js b/ghost/api-framework/lib/frame.js new file mode 100644 index 00000000000..46dde5d6da1 --- /dev/null +++ b/ghost/api-framework/lib/frame.js @@ -0,0 +1,95 @@ +const debug = require('@tryghost/debug')('api:shared:frame'); +const _ = require('lodash'); + +/** + * @description The "frame" holds all information of a request. + * + * Each party can modify the frame by reference. + * A request hits a lot of stages in the API implementation and that's why modification by reference was the + * easiest to use. We always have access to the original input, we never loose track of it. + */ +class Frame { + constructor(obj = {}) { + this.original = obj; + + /** + * options: Query params, url params, context and custom options + * data: Body or if the ctrl wants query/url params inside body + * user: Logged in user + * file: Uploaded file + * files: Uploaded files + * apiType: Content or admin api access + */ + this.options = {}; + this.data = {}; + this.user = {}; + this.file = {}; + this.files = []; + this.apiType = null; + } + + /** + * @description Configure the frame. + * + * If you instantiate a new frame, all the data you pass in, land in `this.original`. This is helpful + * for debugging to see what the original input was. + * + * This function will prepare the incoming data for further processing. + * Based on the API ctrl implemented, this fn will pick allowed properties to either options or data. + */ + configure(apiConfig) { + debug('configure'); + + if (apiConfig.options) { + if (typeof apiConfig.options === 'function') { + apiConfig.options = apiConfig.options(this); + } + + 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')) { + Object.assign(this.options, _.pick(this.original.params, apiConfig.options)); + } + + if (Object.prototype.hasOwnProperty.call(this.original, 'options')) { + Object.assign(this.options, _.pick(this.original.options, apiConfig.options)); + } + } + + this.options.context = this.original.context; + + if (this.original.body && Object.keys(this.original.body).length) { + this.data = _.cloneDeep(this.original.body); + } else { + if (apiConfig.data) { + if (typeof apiConfig.data === 'function') { + apiConfig.data = apiConfig.data(this); + } + + 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')) { + Object.assign(this.data, _.pick(this.original.params, apiConfig.data)); + } + + if (Object.prototype.hasOwnProperty.call(this.original, 'options')) { + Object.assign(this.data, _.pick(this.original.options, apiConfig.data)); + } + } + } + + this.user = this.original.user; + this.file = this.original.file; + this.files = this.original.files; + + debug('original', this.original); + debug('options', this.options); + debug('data', this.data); + } +} + +module.exports = Frame; diff --git a/ghost/api-framework/lib/headers.js b/ghost/api-framework/lib/headers.js new file mode 100644 index 00000000000..7147b2a1fed --- /dev/null +++ b/ghost/api-framework/lib/headers.js @@ -0,0 +1,152 @@ +const url = require('url'); +const debug = require('@tryghost/debug')('api:shared:headers'); +const Promise = require('bluebird'); +const INVALIDATE_ALL = '/*'; + +const cacheInvalidate = (result, options = {}) => { + let value = options.value; + + return { + 'X-Cache-Invalidate': value || INVALIDATE_ALL + }; +}; + +const disposition = { + /** + * @description Generate CSV header. + * + * @param {Object} result - API response + * @param {Object} options + * @return {Object} + */ + csv(result, options = {}) { + let value = options.value; + + if (typeof options.value === 'function') { + value = options.value(); + } + + return { + 'Content-Disposition': `Attachment; filename="${value}"`, + 'Content-Type': 'text/csv' + }; + }, + + /** + * @description Generate JSON header. + * + * @param {Object} result - API response + * @param {Object} options + * @return {Object} + */ + json(result, options = {}) { + return { + 'Content-Disposition': `Attachment; filename="${options.value}"`, + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(JSON.stringify(result)) + }; + }, + + /** + * @description Generate YAML header. + * + * @param {Object} result - API response + * @param {Object} options + * @return {Object} + */ + yaml(result, options = {}) { + return { + 'Content-Disposition': `Attachment; filename="${options.value}"`, + 'Content-Type': 'application/yaml', + 'Content-Length': Buffer.byteLength(JSON.stringify(result)) + }; + }, + + /** + * @description Content Disposition Header + * + * Create a header that invokes the 'Save As' dialog in the browser when exporting the database to file. The 'filename' + * parameter is governed by [RFC6266](http://tools.ietf.org/html/rfc6266#section-4.3). + * + * For encoding whitespace and non-ISO-8859-1 characters, you MUST use the "filename*=" attribute, NOT "filename=". + * Ideally, both. Examples: http://tools.ietf.org/html/rfc6266#section-5 + * + * We'll use ISO-8859-1 characters here to keep it simple. + * + * @see http://tools.ietf.org/html/rfc598 + */ + file(result, options = {}) { + return Promise.resolve() + .then(() => { + let value = options.value; + + if (typeof options.value === 'function') { + value = options.value(); + } + + return value; + }) + .then((filename) => { + return { + 'Content-Disposition': `Attachment; filename="${filename}"` + }; + }); + } +}; + +module.exports = { + /** + * @description Get header based on ctrl configuration. + * + * @param {Object} result - API response + * @param {Object} apiConfigHeaders + * @param {Object} frame + * @return {Promise} + */ + async get(result, apiConfigHeaders = {}, frame) { + let headers = {}; + + if (apiConfigHeaders.disposition) { + const dispositionHeader = await disposition[apiConfigHeaders.disposition.type](result, apiConfigHeaders.disposition); + + if (dispositionHeader) { + Object.assign(headers, dispositionHeader); + } + } + + if (apiConfigHeaders.cacheInvalidate) { + const cacheInvalidationHeader = cacheInvalidate(result, apiConfigHeaders.cacheInvalidate); + + if (cacheInvalidationHeader) { + Object.assign(headers, cacheInvalidationHeader); + } + } + + const locationHeaderDisabled = apiConfigHeaders && apiConfigHeaders.location === false; + const hasFrameData = frame + && (frame.method === 'add') + && result[frame.docName] + && result[frame.docName][0] + && result[frame.docName][0].id; + + if (!locationHeaderDisabled && hasFrameData) { + 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 += '/'; + } + locationURL += `${resourceId}/`; + + const locationHeader = { + Location: locationURL + }; + + Object.assign(headers, locationHeader); + } + + debug(headers); + return headers; + } +}; diff --git a/ghost/api-framework/lib/http.js b/ghost/api-framework/lib/http.js new file mode 100644 index 00000000000..d47aa85bb80 --- /dev/null +++ b/ghost/api-framework/lib/http.js @@ -0,0 +1,127 @@ +const url = require('url'); +const debug = require('@tryghost/debug')('api:shared:http'); + +const Frame = require('./frame'); +const headers = require('./headers'); + +/** + * @description HTTP wrapper. + * + * This wrapper is used in the routes definition (see web/). + * The wrapper receives the express request, prepares the frame and forwards the request to the pipeline. + * + * @param {Function} apiImpl - Pipeline wrapper, which executes the target ctrl function. + * @return {Function} + */ +const http = (apiImpl) => { + return async (req, res, next) => { + debug(`External API request to ${req.url}`); + let apiKey = null; + let integration = null; + let user = null; + + if (req.api_key) { + apiKey = { + id: req.api_key.get('id'), + type: req.api_key.get('type') + }; + integration = { + id: req.api_key.get('integration_id') + }; + } + + if (req.user && req.user.id) { + user = req.user.id; + } + + const frame = new Frame({ + body: req.body, + file: req.file, + files: req.files, + query: req.query, + params: req.params, + user: req.user, + session: req.session, + url: { + host: req.vhost ? req.vhost.host : req.get('host'), + pathname: url.parse(req.originalUrl || req.url).pathname, + secure: req.secure + }, + context: { + api_key: apiKey, + user: user, + integration: integration, + member: (req.member || null) + } + }); + + frame.configure({ + options: apiImpl.options, + data: apiImpl.data + }); + + try { + const result = await apiImpl(frame); + + debug(`External API request to ${frame.docName}.${frame.method}`); + const apiHeaders = await headers.get(result, apiImpl.headers, frame) || {}; + + // CASE: api ctrl wants to handle the express response (e.g. streams) + if (typeof result === 'function') { + debug('ctrl function call'); + return result(req, res, next); + } + + let statusCode = 200; + if (typeof apiImpl.statusCode === 'function') { + statusCode = apiImpl.statusCode(result); + } else if (apiImpl.statusCode) { + statusCode = apiImpl.statusCode; + } + + res.status(statusCode); + + // CASE: generate headers based on the api ctrl configuration + res.set(apiHeaders); + + const send = (format) => { + if (format === 'plain') { + debug('plain text response'); + return res.send(result); + } + + debug('json response'); + res.json(result || {}); + }; + + let responseFormat; + + if (apiImpl.response){ + if (typeof apiImpl.response.format === 'function') { + const apiResponseFormat = apiImpl.response.format(); + + if (apiResponseFormat.then) { // is promise + return apiResponseFormat.then((formatName) => { + send(formatName); + }); + } else { + responseFormat = apiResponseFormat; + } + } else { + responseFormat = apiImpl.response.format; + } + } + + send(responseFormat); + } catch (err) { + req.frameOptions = { + docName: frame.docName, + method: frame.method + }; + + next(err); + } + }; +}; + +module.exports = http; diff --git a/ghost/api-framework/lib/pipeline.js b/ghost/api-framework/lib/pipeline.js new file mode 100644 index 00000000000..14dd1365e05 --- /dev/null +++ b/ghost/api-framework/lib/pipeline.js @@ -0,0 +1,262 @@ +const debug = require('@tryghost/debug')('api:shared:pipeline'); +const Promise = require('bluebird'); +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 STAGES = { + validation: { + /** + * @description Input validation. + * + * We call the shared validator which runs the request through: + * + * 1. Shared validator + * 2. Custom API validators + * + * @param {Object} apiUtils - Local utils of target API version. + * @param {Object} apiConfig - Docname & Method of ctrl. + * @param {Object} apiImpl - Controller configuration. + * @param {Object} frame + * @return {Promise} + */ + input(apiUtils, apiConfig, apiImpl, frame) { + debug('stages: validation'); + const tasks = []; + + // CASE: do validation completely yourself + if (typeof apiImpl.validation === 'function') { + debug('validation function call'); + return apiImpl.validation(frame); + } + + tasks.push(function doValidation() { + return validators.handle.input( + Object.assign({}, apiConfig, apiImpl.validation), + apiUtils.validators.input, + frame + ); + }); + + return sequence(tasks); + } + }, + + serialisation: { + /** + * @description Input Serialisation. + * + * We call the shared serializer which runs the request through: + * + * 1. Shared serializers + * 2. Custom API serializers + * + * @param {Object} apiUtils - Local utils of target API version. + * @param {Object} apiConfig - Docname & Method of ctrl. + * @param {Object} apiImpl - Controller configuration. + * @param {Object} frame + * @return {Promise} + */ + input(apiUtils, apiConfig, apiImpl, frame) { + debug('stages: input serialisation'); + return serializers.handle.input( + Object.assign({data: apiImpl.data}, apiConfig), + apiUtils.serializers.input, + frame + ); + }, + + /** + * @description Output Serialisation. + * + * We call the shared serializer which runs the request through: + * + * 1. Shared serializers + * 2. Custom API serializers + * + * @param {Object} apiUtils - Local utils of target API version. + * @param {Object} apiConfig - Docname & Method of ctrl. + * @param {Object} apiImpl - Controller configuration. + * @param {Object} frame + * @return {Promise} + */ + output(response, apiUtils, apiConfig, apiImpl, frame) { + debug('stages: output serialisation'); + return serializers.handle.output(response, apiConfig, apiUtils.serializers.output, frame); + } + }, + + /** + * @description Permissions stage. + * + * We call the target API implementation of permissions. + * Permissions implementation can change across API versions. + * There is no shared implementation right now. + * + * @param {Object} apiUtils - Local utils of target API version. + * @param {Object} apiConfig - Docname & Method of ctrl. + * @param {Object} apiImpl - Controller configuration. + * @param {Object} frame + * @return {Promise} + */ + permissions(apiUtils, apiConfig, apiImpl, frame) { + 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')) { + return Promise.reject(new errors.IncorrectUsageError()); + } + + // CASE: handle permissions completely yourself + if (typeof apiImpl.permissions === 'function') { + debug('permissions function call'); + return apiImpl.permissions(frame); + } + + // CASE: skip stage completely + if (apiImpl.permissions === false) { + debug('disabled permissions'); + return Promise.resolve(); + } + + if (typeof apiImpl.permissions === 'object' && apiImpl.permissions.before) { + tasks.push(function beforePermissions() { + return apiImpl.permissions.before(frame); + }); + } + + tasks.push(function doPermissions() { + return apiUtils.permissions.handle( + Object.assign({}, apiConfig, apiImpl.permissions), + frame + ); + }); + + return sequence(tasks); + }, + + /** + * @description Execute controller & receive model response. + * + * @param {Object} apiUtils - Local utils of target API version. + * @param {Object} apiConfig - Docname & Method of ctrl. + * @param {Object} apiImpl - Controller configuration. + * @param {Object} frame + * @return {Promise} + */ + query(apiUtils, apiConfig, apiImpl, frame) { + debug('stages: query'); + + if (!apiImpl.query) { + return Promise.reject(new errors.IncorrectUsageError()); + } + + return apiImpl.query(frame); + } +}; + +/** + * @description The pipeline runs the request through all stages (validation, serialisation, permissions). + * + * The target API version calls the pipeline and wraps the actual ctrl implementation to be able to + * run the request through various stages before hitting the controller. + * + * The stages are executed in the following order: + * + * 1. Input validation - General & schema validation + * 2. Input serialisation - Modification of incoming data e.g. force filters, auto includes, url transformation etc. + * 3. Permissions - Runs after validation & serialisation because the body structure must be valid (see unsafeAttrs) + * 4. Controller - Execute the controller implementation & receive model response. + * 5. Output Serialisation - Output formatting, Deprecations, Extra attributes etc... + * + * @param {Function} apiController + * @param {Object} apiUtils - Local utils (validation & serialisation) from target API version + * @param {String} [apiType] - Content or Admin API access + * @return {Function} + */ +const pipeline = (apiController, apiUtils, apiType) => { + const keys = Object.keys(apiController); + + // CASE: api controllers are objects with configuration. + // We have to ensure that we expose a functional interface e.g. `api.posts.add` has to be available. + return keys.reduce((obj, key) => { + const docName = apiController.docName; + const method = key; + + const apiImpl = _.cloneDeep(apiController)[key]; + + obj[key] = function wrapper() { + const apiConfig = {docName, method}; + let options; + let data; + let frame; + + if (arguments.length === 2) { + data = arguments[0]; + options = arguments[1]; + } else if (arguments.length === 1) { + options = arguments[0] || {}; + } else { + options = {}; + } + + // CASE: http helper already creates it's own frame. + if (!(options instanceof Frame)) { + debug(`Internal API request for ${docName}.${method}`); + frame = new Frame({ + body: data, + options: _.omit(options, 'context'), + context: options.context || {} + }); + + frame.configure({ + options: apiImpl.options, + 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'); + return apiImpl(frame); + } + + frame.apiType = apiType; + frame.docName = docName; + frame.method = method; + + return Promise.resolve() + .then(() => { + return STAGES.validation.input(apiUtils, apiConfig, apiImpl, frame); + }) + .then(() => { + return STAGES.serialisation.input(apiUtils, apiConfig, apiImpl, frame); + }) + .then(() => { + return STAGES.permissions(apiUtils, apiConfig, apiImpl, frame); + }) + .then(() => { + return STAGES.query(apiUtils, apiConfig, apiImpl, frame); + }) + .then((response) => { + return STAGES.serialisation.output(response, apiUtils, apiConfig, apiImpl, frame); + }) + .then(() => { + return frame.response; + }); + }; + + Object.assign(obj[key], apiImpl); + return obj; + }, {}); +}; + +module.exports = pipeline; +module.exports.STAGES = STAGES; diff --git a/ghost/api-framework/lib/serializers/handle.js b/ghost/api-framework/lib/serializers/handle.js new file mode 100644 index 00000000000..39afa020ae3 --- /dev/null +++ b/ghost/api-framework/lib/serializers/handle.js @@ -0,0 +1,140 @@ +const debug = require('@tryghost/debug')('api:shared:serializers:handle'); +const Promise = require('bluebird'); +const {sequence} = require('@tryghost/promise'); +const errors = require('@tryghost/errors'); + +/** + * @description Shared input serialization handler. + * + * The shared input handler runs the request through all the validation steps. + * + * 1. Shared serialization + * 2. API serialization + * + * @param {Object} apiConfig - Docname + method of the ctrl + * @param {Object} apiSerializers - Target API serializers + * @param {Object} frame + */ +module.exports.input = (apiConfig, apiSerializers, frame) => { + debug('input'); + + const tasks = []; + const sharedSerializers = require('./input'); + + if (!apiConfig) { + return Promise.reject(new errors.IncorrectUsageError()); + } + + if (!apiSerializers) { + return Promise.reject(new errors.IncorrectUsageError()); + } + + // ##### SHARED ALL SERIALIZATION + + tasks.push(function serializeAllShared() { + return sharedSerializers.all.all(apiConfig, frame); + }); + + if (sharedSerializers.all[apiConfig.method]) { + tasks.push(function serializeAllShared() { + return sharedSerializers.all[apiConfig.method](apiConfig, frame); + }); + } + + // ##### API VERSION RESOURCE SERIALIZATION + + if (apiSerializers.all) { + tasks.push(function serializeOptionsShared() { + return apiSerializers.all(apiConfig, frame); + }); + } + + if (apiSerializers[apiConfig.docName]) { + if (apiSerializers[apiConfig.docName].all) { + tasks.push(function serializeOptionsShared() { + return apiSerializers[apiConfig.docName].all(apiConfig, frame); + }); + } + + if (apiSerializers[apiConfig.docName][apiConfig.method]) { + tasks.push(function serializeOptionsShared() { + return apiSerializers[apiConfig.docName][apiConfig.method](apiConfig, frame); + }); + } + } + + debug(tasks); + return sequence(tasks); +}; + +const getBestMatchSerializer = function (apiSerializers, docName, method) { + if (apiSerializers[docName] && apiSerializers[docName][method]) { + debug(`Calling ${docName}.${method}`); + return apiSerializers[docName][method].bind(apiSerializers[docName]); + } else if (apiSerializers[docName] && apiSerializers[docName].all) { + debug(`Calling ${docName}.all`); + return apiSerializers[docName].all.bind(apiSerializers[docName]); + } + + debug(`Returning as-is`); + return false; +}; + +/** + * @description Shared output serialization handler. + * + * The shared output handler runs the request through all the validation steps. + * + * 1. Shared serialization + * 2. API serialization + * + * @param {Object} response - API response + * @param {Object} apiConfig - Docname + method of the ctrl + * @param {Object} apiSerializers - Target API serializers + * @param {Object} frame + */ +module.exports.output = (response = {}, apiConfig, apiSerializers, frame) => { + debug('output'); + + const tasks = []; + + if (!apiConfig) { + return Promise.reject(new errors.IncorrectUsageError()); + } + + if (!apiSerializers) { + return Promise.reject(new errors.IncorrectUsageError()); + } + + // ##### API VERSION RESOURCE SERIALIZATION + + if (apiSerializers.all && apiSerializers.all.before) { + tasks.push(function allSerializeBefore() { + return apiSerializers.all.before(response, apiConfig, frame); + }); + } + + const customSerializer = getBestMatchSerializer(apiSerializers, apiConfig.docName, apiConfig.method); + const defaultSerializer = getBestMatchSerializer(apiSerializers, 'default', apiConfig.method); + + if (customSerializer) { + // CASE: custom serializer exists + tasks.push(function doCustomSerializer() { + return customSerializer(response, apiConfig, frame); + }); + } else if (defaultSerializer) { + // CASE: Fall back to default serializer + tasks.push(function doDefaultSerializer() { + return defaultSerializer(response, apiConfig, frame); + }); + } + + if (apiSerializers.all && apiSerializers.all.after) { + tasks.push(function allSerializeAfter() { + return apiSerializers.all.after(apiConfig, frame); + }); + } + + debug(tasks); + return sequence(tasks); +}; diff --git a/ghost/api-framework/lib/serializers/index.js b/ghost/api-framework/lib/serializers/index.js new file mode 100644 index 00000000000..ba10b3cbeeb --- /dev/null +++ b/ghost/api-framework/lib/serializers/index.js @@ -0,0 +1,13 @@ +module.exports = { + get handle() { + return require('./handle'); + }, + + get input() { + return require('./input'); + }, + + get output() { + return require('./output'); + } +}; diff --git a/ghost/api-framework/lib/serializers/input/all.js b/ghost/api-framework/lib/serializers/input/all.js new file mode 100644 index 00000000000..a372dae2794 --- /dev/null +++ b/ghost/api-framework/lib/serializers/input/all.js @@ -0,0 +1,41 @@ +const debug = require('@tryghost/debug')('api:shared:serializers:input:all'); +const _ = require('lodash'); +const utils = require('../../utils'); + +const INTERNAL_OPTIONS = ['transacting', 'forUpdate']; + +/** + * @description Shared serializer for all requests. + * + * Transforms certain options from API notation into model readable language/notation. + * + * e.g. API uses "include", but model layer uses "withRelated". + */ +module.exports = { + all(apiConfig, frame) { + debug('serialize all'); + + if (frame.options.include) { + frame.options.withRelated = utils.options.trimAndLowerCase(frame.options.include); + delete frame.options.include; + } + + if (frame.options.fields) { + frame.options.columns = utils.options.trimAndLowerCase(frame.options.fields); + delete frame.options.fields; + } + + if (frame.options.formats) { + frame.options.formats = utils.options.trimAndLowerCase(frame.options.formats); + } + + if (frame.options.formats && frame.options.columns) { + frame.options.columns = frame.options.columns.concat(frame.options.formats); + } + + if (!frame.options.context.internal) { + debug('omit internal options'); + frame.options = _.omit(frame.options, INTERNAL_OPTIONS); + } + } +}; diff --git a/ghost/api-framework/lib/serializers/input/index.js b/ghost/api-framework/lib/serializers/input/index.js new file mode 100644 index 00000000000..7b25869f4f5 --- /dev/null +++ b/ghost/api-framework/lib/serializers/input/index.js @@ -0,0 +1,5 @@ +module.exports = { + get all() { + return require('./all'); + } +}; diff --git a/ghost/api-framework/lib/serializers/output/index.js b/ghost/api-framework/lib/serializers/output/index.js new file mode 100644 index 00000000000..f053ebf7976 --- /dev/null +++ b/ghost/api-framework/lib/serializers/output/index.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/ghost/api-framework/lib/utils/index.js b/ghost/api-framework/lib/utils/index.js new file mode 100644 index 00000000000..ce0fc5c4305 --- /dev/null +++ b/ghost/api-framework/lib/utils/index.js @@ -0,0 +1,5 @@ +module.exports = { + get options() { + return require('./options'); + } +}; diff --git a/ghost/api-framework/lib/utils/options.js b/ghost/api-framework/lib/utils/options.js new file mode 100644 index 00000000000..22dd5759258 --- /dev/null +++ b/ghost/api-framework/lib/utils/options.js @@ -0,0 +1,23 @@ +const _ = require('lodash'); + +/** + * @description Helper function to prepare params for internal usages. + * + * e.g. "a,B,c" -> ["a", "b", "c"] + * + * @param {String} params + * @return {Array} + */ +const trimAndLowerCase = (params) => { + params = params || ''; + + if (_.isString(params)) { + params = params.split(','); + } + + return params.map((item) => { + return item.trim().toLowerCase(); + }); +}; + +module.exports.trimAndLowerCase = trimAndLowerCase; diff --git a/ghost/api-framework/lib/validators/handle.js b/ghost/api-framework/lib/validators/handle.js new file mode 100644 index 00000000000..27eb0da88ba --- /dev/null +++ b/ghost/api-framework/lib/validators/handle.js @@ -0,0 +1,68 @@ +const debug = require('@tryghost/debug')('api:shared:validators:handle'); +const Promise = require('bluebird'); +const errors = require('@tryghost/errors'); +const {sequence} = require('@tryghost/promise'); + +/** + * @description Shared input validation handler. + * + * The shared validation handler runs the request through all the validation steps. + * + * 1. Shared validation + * 2. API validation + * + * @param {Object} apiConfig - Docname + method of the ctrl + * @param {Object} apiValidators - Target API validators + * @param {Object} frame + */ +module.exports.input = (apiConfig, apiValidators, frame) => { + debug('input begin'); + + const tasks = []; + const sharedValidators = require('./input'); + + if (!apiValidators) { + return Promise.reject(new errors.IncorrectUsageError()); + } + + if (!apiConfig) { + return Promise.reject(new errors.IncorrectUsageError()); + } + + // ##### SHARED ALL VALIDATION + + tasks.push(function allShared() { + return sharedValidators.all.all(apiConfig, frame); + }); + + if (sharedValidators.all[apiConfig.method]) { + tasks.push(function allShared() { + return sharedValidators.all[apiConfig.method](apiConfig, frame); + }); + } + + // ##### API VERSION VALIDATION + + if (apiValidators.all) { + tasks.push(function allAPIVersion() { + return apiValidators.all[apiConfig.method](apiConfig, frame); + }); + } + + if (apiValidators[apiConfig.docName]) { + if (apiValidators[apiConfig.docName].all) { + tasks.push(function docNameAll() { + return apiValidators[apiConfig.docName].all(apiConfig, frame); + }); + } + + if (apiValidators[apiConfig.docName][apiConfig.method]) { + tasks.push(function docNameMethod() { + return apiValidators[apiConfig.docName][apiConfig.method](apiConfig, frame); + }); + } + } + + debug('input ready'); + return sequence(tasks); +}; diff --git a/ghost/api-framework/lib/validators/index.js b/ghost/api-framework/lib/validators/index.js new file mode 100644 index 00000000000..7830fcfee49 --- /dev/null +++ b/ghost/api-framework/lib/validators/index.js @@ -0,0 +1,9 @@ +module.exports = { + get handle() { + return require('./handle'); + }, + + get input() { + return require('./input'); + } +}; diff --git a/ghost/api-framework/lib/validators/input/all.js b/ghost/api-framework/lib/validators/input/all.js new file mode 100644 index 00000000000..f6061d1c471 --- /dev/null +++ b/ghost/api-framework/lib/validators/input/all.js @@ -0,0 +1,213 @@ +const debug = require('@tryghost/debug')('api:shared:validators:input:all'); +const _ = require('lodash'); +const Promise = require('bluebird'); +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.' +}; + +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}, + name: {}, + email: {isEmail: true}, + filter: false, + context: false, + forUpdate: false, + transacting: false, + include: false, + formats: false +}; + +const validate = (config, attrs) => { + let errors = []; + + _.each(config, (value, key) => { + if (value.required && !attrs[key]) { + errors.push(new ValidationError({ + message: tpl(messages.validationFailed, { + validationName: 'FieldIsRequired', + key: key + }) + })); + } + }); + + _.each(attrs, (value, key) => { + debug(key, value); + + if (GLOBAL_VALIDATORS[key]) { + debug('global validation'); + errors = errors.concat(validator.validate(value, key, GLOBAL_VALIDATORS[key])); + } + + if (config && config[key]) { + const allowedValues = Array.isArray(config[key]) ? config[key] : config[key].values; + + if (allowedValues) { + 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 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)); + return; + } + + errors.push(new ValidationError({ + message: tpl(messages.validationFailed, { + validationName: 'AllowedValues', + key: key + }) + })); + } + } + } + }); + + return errors; +}; + +module.exports = { + all(apiConfig, frame) { + debug('validate all'); + + let validationErrors = validate(apiConfig.options, frame.options); + + if (!_.isEmpty(validationErrors)) { + return Promise.reject(validationErrors[0]); + } + + return Promise.resolve(); + }, + + browse(apiConfig, frame) { + debug('validate browse'); + + let validationErrors = []; + + if (frame.data) { + validationErrors = validate(apiConfig.data, frame.data); + } + + if (!_.isEmpty(validationErrors)) { + return Promise.reject(validationErrors[0]); + } + }, + + read() { + debug('validate read'); + return this.browse(...arguments); + }, + + add(apiConfig, frame) { + 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}) + })); + } + } + + const jsonpath = require('jsonpath'); + + if (apiConfig.data) { + const missedDataProperties = []; + const nilDataProperties = []; + + _.each(apiConfig.data, (value, key) => { + if (jsonpath.query(frame.data[apiConfig.docName][0], key).length === 0) { + missedDataProperties.push(key); + } else if (_.isNil(frame.data[apiConfig.docName][0][key])) { + nilDataProperties.push(key); + } + }); + + if (missedDataProperties.length) { + 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) + }) + })); + } + } + }, + + edit(apiConfig, frame) { + debug('validate edit'); + const result = this.add(...arguments); + + if (result instanceof Promise) { + return result; + } + + // NOTE: this block should be removed completely once JSON Schema validations + // 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) + })); + } + } + }, + + changePassword() { + debug('validate changePassword'); + return this.add(...arguments); + }, + + resetPassword() { + debug('validate resetPassword'); + return this.add(...arguments); + }, + + setup() { + debug('validate setup'); + return this.add(...arguments); + }, + + publish() { + debug('validate schedule'); + return this.browse(...arguments); + } +}; diff --git a/ghost/api-framework/lib/validators/input/index.js b/ghost/api-framework/lib/validators/input/index.js new file mode 100644 index 00000000000..7b25869f4f5 --- /dev/null +++ b/ghost/api-framework/lib/validators/input/index.js @@ -0,0 +1,5 @@ +module.exports = { + get all() { + return require('./all'); + } +}; diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json new file mode 100644 index 00000000000..585a9421061 --- /dev/null +++ b/ghost/api-framework/package.json @@ -0,0 +1,34 @@ +{ + "name": "@tryghost/api-framework", + "version": "0.0.0", + "repository": "https://github.com/TryGhost/Ghost/tree/main/packages/api-framework", + "author": "Ghost Foundation", + "private": true, + "main": "index.js", + "scripts": { + "dev": "echo \"Implement me!\"", + "test": "NODE_ENV=testing c8 --all --reporter text --reporter cobertura mocha './test/**/*.test.js'", + "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" + }, + "files": [ + "index.js", + "lib" + ], + "devDependencies": { + "c8": "7.12.0", + "mocha": "10.0.0", + "should": "13.2.3", + "sinon": "14.0.0" + }, + "dependencies": { + "@tryghost/debug": "0.1.18", + "@tryghost/errors": "1.2.15", + "@tryghost/promise": "0.1.21", + "@tryghost/tpl": "0.1.18", + "@tryghost/validator": "0.1.27", + "jsonpath": "1.1.1", + "lodash": "4.17.21" + } +} diff --git a/ghost/api-framework/test/.eslintrc.js b/ghost/api-framework/test/.eslintrc.js new file mode 100644 index 00000000000..829b601eb0a --- /dev/null +++ b/ghost/api-framework/test/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: ['ghost'], + extends: [ + 'plugin:ghost/test' + ] +}; diff --git a/ghost/api-framework/test/frame.test.js b/ghost/api-framework/test/frame.test.js new file mode 100644 index 00000000000..bdbd645aa26 --- /dev/null +++ b/ghost/api-framework/test/frame.test.js @@ -0,0 +1,101 @@ +const should = require('should'); +const shared = require('../'); + +describe('Unit: api/shared/frame', function () { + it('constructor', function () { + const frame = new shared.Frame(); + Object.keys(frame).should.eql([ + 'original', + 'options', + 'data', + 'user', + 'file', + 'files', + 'apiType' + ]); + }); + + 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'} + }; + + const frame = new shared.Frame(original); + + frame.configure({}); + + should.exist(frame.options.context.user); + should.not.exist(frame.options.include); + should.not.exist(frame.options.filter); + should.not.exist(frame.options.id); + should.not.exist(frame.options.soup); + + should.exist(frame.data.posts); + }); + + it('transform with query', function () { + const original = { + 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'] + }); + + should.exist(frame.options.context.user); + should.exist(frame.options.include); + should.exist(frame.options.filter); + should.exist(frame.options.id); + should.not.exist(frame.options.soup); + + should.exist(frame.data.posts); + }); + + it('transform', function () { + const original = { + context: {user: 'id'}, + options: { + slug: 'slug' + } + }; + + const frame = new shared.Frame(original); + + frame.configure({ + options: ['include', 'filter', 'slug'] + }); + + should.exist(frame.options.context.user); + should.exist(frame.options.slug); + }); + + it('transform with data', function () { + const original = { + context: {user: 'id'}, + options: { + id: 'id' + }, + body: {} + }; + + const frame = new shared.Frame(original); + + frame.configure({ + data: ['id'] + }); + + should.exist(frame.options.context.user); + should.not.exist(frame.options.id); + should.exist(frame.data.id); + }); + }); +}); diff --git a/ghost/api-framework/test/headers.test.js b/ghost/api-framework/test/headers.test.js new file mode 100644 index 00000000000..ecdbf5c2d33 --- /dev/null +++ b/ghost/api-framework/test/headers.test.js @@ -0,0 +1,142 @@ +const shared = require('../'); + +describe('Unit: api/shared/headers', function () { + it('empty headers config', function () { + return shared.headers.get().then((result) => { + result.should.eql({}); + }); + }); + + describe('config.disposition', function () { + it('json', function () { + return shared.headers.get({}, {disposition: {type: 'json', value: 'value'}}) + .then((result) => { + result.should.eql({ + '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'}}) + .then((result) => { + result.should.eql({ + 'Content-Disposition': 'Attachment; filename="my.csv"', + 'Content-Type': 'text/csv' + }); + }); + }); + + it('yaml', function () { + return shared.headers.get('yaml file', {disposition: {type: 'yaml', value: 'my.yaml'}}) + .then((result) => { + result.should.eql({ + '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}) + .then((result) => { + result.should.eql({ + 'X-Cache-Invalidate': '/*' + }); + }); + }); + + it('custom value', function () { + return shared.headers.get({}, {cacheInvalidate: {value: 'value'}}) + .then((result) => { + result.should.eql({ + 'X-Cache-Invalidate': 'value' + }); + }); + }); + }); + + describe('location header', function () { + it('adds header when all needed data is present', function () { + const apiResult = { + posts: [{ + id: 'id_value' + }] + }; + + const apiConfigHeaders = {}; + const frame = { + docName: 'posts', + method: 'add', + original: { + url: { + host: 'example.com', + pathname: `/api/content/posts/` + } + } + }; + + return shared.headers.get(apiResult, apiConfigHeaders, frame) + .then((result) => { + result.should.eql({ + // 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 and resolves header to correct url when pathname does not contain backslash in the end', function () { + const apiResult = { + posts: [{ + id: 'id_value' + }] + }; + + const apiConfigHeaders = {}; + const frame = { + docName: 'posts', + method: 'add', + original: { + url: { + host: 'example.com', + pathname: `/api/content/posts` + } + } + }; + + return shared.headers.get(apiResult, apiConfigHeaders, frame) + .then((result) => { + result.should.eql({ + // 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 () { + const apiResult = {}; + + const apiConfigHeaders = {}; + const frame = { + docName: 'posts', + method: 'add', + original: { + url: { + host: 'example.com', + pathname: `/api/content/posts/` + } + } + }; + + return shared.headers.get(apiResult, apiConfigHeaders, frame) + .then((result) => { + result.should.eql({}); + }); + }); + }); +}); diff --git a/ghost/api-framework/test/http.test.js b/ghost/api-framework/test/http.test.js new file mode 100644 index 00000000000..7de8495fde8 --- /dev/null +++ b/ghost/api-framework/test/http.test.js @@ -0,0 +1,90 @@ +const should = require('should'); +const sinon = require('sinon'); +const shared = require('../'); + +describe('Unit: api/shared/http', function () { + let req; + let res; + let next; + + beforeEach(function () { + req = sinon.stub(); + res = sinon.stub(); + next = sinon.stub(); + + req.body = { + a: 'a' + }; + req.vhost = { + host: 'example.com' + }; + 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(); + }); + + afterEach(function () { + sinon.restore(); + }); + + it('check options', function () { + const apiImpl = sinon.stub().resolves(); + shared.http(apiImpl)(req, res, next); + + Object.keys(apiImpl.args[0][0]).should.eql([ + 'original', + 'options', + 'data', + 'user', + 'file', + 'files', + 'apiType' + ]); + + apiImpl.args[0][0].data.should.eql({a: 'a'}); + apiImpl.args[0][0].options.should.eql({ + context: { + api_key: null, + integration: null, + user: null, + member: null + } + }); + }); + + it('api response is fn', function (done) { + const response = sinon.stub().callsFake(function (_req, _res, _next) { + should.exist(_req); + should.exist(_res); + should.exist(_next); + apiImpl.calledOnce.should.be.true(); + _res.json.called.should.be.false(); + done(); + }); + + const apiImpl = sinon.stub().resolves(response); + shared.http(apiImpl)(req, res, next); + }); + + it('api response is fn (data)', function (done) { + const apiImpl = sinon.stub().resolves('data'); + + next.callsFake(done); + + res.json.callsFake(function () { + shared.headers.get.calledOnce.should.be.true(); + res.status.calledOnce.should.be.true(); + res.send.called.should.be.false(); + done(); + }); + + shared.http(apiImpl)(req, res, next); + }); +}); diff --git a/ghost/api-framework/test/pipeline.test.js b/ghost/api-framework/test/pipeline.test.js new file mode 100644 index 00000000000..4f139d864e2 --- /dev/null +++ b/ghost/api-framework/test/pipeline.test.js @@ -0,0 +1,253 @@ +const errors = require('@tryghost/errors'); +const should = require('should'); +const sinon = require('sinon'); +const shared = require('../'); + +describe('Unit: api/shared/pipeline', function () { + afterEach(function () { + sinon.restore(); + }); + + describe('stages', function () { + describe('validation', function () { + describe('input', function () { + beforeEach(function () { + sinon.stub(shared.validators.handle, 'input').resolves(); + }); + + it('do it yourself', function () { + const apiUtils = {}; + const apiConfig = {}; + const apiImpl = { + validation: sinon.stub().resolves('response') + }; + const frame = {}; + + return shared.pipeline.STAGES.validation.input(apiUtils, apiConfig, apiImpl, frame) + .then((response) => { + response.should.eql('response'); + + apiImpl.validation.calledOnce.should.be.true(); + shared.validators.handle.input.called.should.be.false(); + }); + }); + + it('default', function () { + const apiUtils = { + validators: { + input: { + posts: {} + } + } + }; + const apiConfig = { + docName: 'posts' + }; + const apiImpl = { + options: ['include'], + validation: { + options: { + include: { + required: true + } + } + } + }; + const frame = { + options: {} + }; + + return shared.pipeline.STAGES.validation.input(apiUtils, apiConfig, apiImpl, frame) + .then(() => { + shared.validators.handle.input.calledOnce.should.be.true(); + shared.validators.handle.input.calledWith( + { + docName: 'posts', + options: { + include: { + required: true + } + } + }, + { + posts: {} + }, + { + options: {} + }).should.be.true(); + }); + }); + }); + }); + + describe('permissions', function () { + let apiUtils; + + beforeEach(function () { + apiUtils = { + permissions: { + handle: sinon.stub().resolves() + } + }; + }); + + it('key is missing', function () { + const apiConfig = {}; + const apiImpl = {}; + const frame = {}; + + return shared.pipeline.STAGES.permissions(apiUtils, apiConfig, apiImpl, frame) + .then(Promise.reject) + .catch((err) => { + (err instanceof errors.IncorrectUsageError).should.be.true(); + apiUtils.permissions.handle.called.should.be.false(); + }); + }); + + it('do it yourself', function () { + const apiConfig = {}; + const apiImpl = { + permissions: sinon.stub().resolves('lol') + }; + const frame = {}; + + return shared.pipeline.STAGES.permissions(apiUtils, apiConfig, apiImpl, frame) + .then((response) => { + response.should.eql('lol'); + apiImpl.permissions.calledOnce.should.be.true(); + apiUtils.permissions.handle.called.should.be.false(); + }); + }); + + it('skip stage', function () { + const apiConfig = {}; + const apiImpl = { + permissions: false + }; + const frame = {}; + + return shared.pipeline.STAGES.permissions(apiUtils, apiConfig, apiImpl, frame) + .then(() => { + apiUtils.permissions.handle.called.should.be.false(); + }); + }); + + it('default', function () { + const apiConfig = {}; + const apiImpl = { + permissions: true + }; + const frame = {}; + + return shared.pipeline.STAGES.permissions(apiUtils, apiConfig, apiImpl, frame) + .then(() => { + apiUtils.permissions.handle.calledOnce.should.be.true(); + }); + }); + + it('with permission config', function () { + const apiConfig = { + docName: 'posts' + }; + const apiImpl = { + permissions: { + unsafeAttrs: ['test'] + } + }; + const frame = { + options: {} + }; + + return shared.pipeline.STAGES.permissions(apiUtils, apiConfig, apiImpl, frame) + .then(() => { + apiUtils.permissions.handle.calledOnce.should.be.true(); + apiUtils.permissions.handle.calledWith( + { + docName: 'posts', + unsafeAttrs: ['test'] + }, + { + options: {} + }).should.be.true(); + }); + }); + }); + }); + + 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'); + }); + + it('ensure we receive a callable api controller fn', function () { + const apiController = { + add: {}, + browse: {} + }; + + const apiUtils = {}; + + const result = shared.pipeline(apiController, apiUtils); + result.should.be.an.Object(); + + should.exist(result.add); + should.exist(result.browse); + result.add.should.be.a.Function(); + result.browse.should.be.a.Function(); + }); + + it('call api controller fn', function () { + const apiController = { + add: {} + }; + + const apiUtils = {}; + const result = shared.pipeline(apiController, apiUtils); + + 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() + .then((response) => { + response.should.eql('response'); + + shared.pipeline.STAGES.validation.input.calledOnce.should.be.true(); + shared.pipeline.STAGES.serialisation.input.calledOnce.should.be.true(); + shared.pipeline.STAGES.permissions.calledOnce.should.be.true(); + shared.pipeline.STAGES.query.calledOnce.should.be.true(); + shared.pipeline.STAGES.serialisation.output.calledOnce.should.be.true(); + }); + }); + + it('api controller is fn, not config', function () { + const apiController = { + add() { + return Promise.resolve('response'); + } + }; + + const apiUtils = {}; + const result = shared.pipeline(apiController, apiUtils); + + return result.add() + .then((response) => { + response.should.eql('response'); + + shared.pipeline.STAGES.validation.input.called.should.be.false(); + shared.pipeline.STAGES.serialisation.input.called.should.be.false(); + shared.pipeline.STAGES.permissions.called.should.be.false(); + shared.pipeline.STAGES.query.called.should.be.false(); + shared.pipeline.STAGES.serialisation.output.called.should.be.false(); + }); + }); + }); +}); diff --git a/ghost/api-framework/test/serializers/handle.test.js b/ghost/api-framework/test/serializers/handle.test.js new file mode 100644 index 00000000000..c00b364132f --- /dev/null +++ b/ghost/api-framework/test/serializers/handle.test.js @@ -0,0 +1,412 @@ +const errors = require('@tryghost/errors'); +const Promise = require('bluebird'); +const sinon = require('sinon'); +const shared = require('../../'); + +describe('Unit: api/shared/serializers/handle', function () { + afterEach(function () { + sinon.restore(); + }); + + describe('input', function () { + it('no api config passed', function () { + return shared.serializers.handle.input() + .then(Promise.reject) + .catch((err) => { + (err instanceof errors.IncorrectUsageError).should.be.true(); + }); + }); + + it('no api serializers passed', function () { + return shared.serializers.handle.input({}) + .then(Promise.reject) + .catch((err) => { + (err instanceof errors.IncorrectUsageError).should.be.true(); + }); + }); + + it('ensure default serializers are called with apiConfig and frame', function () { + const allStub = sinon.stub(); + sinon.stub(shared.serializers.input.all, 'all').get(() => allStub); + + const apiSerializers = { + all: sinon.stub().resolves(), + posts: { + all: sinon.stub().resolves(), + browse: sinon.stub().resolves() + } + }; + + const apiConfig = {docName: 'posts', method: 'browse'}; + const frame = {}; + + const stubsToCheck = [ + allStub, + 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); + }); + }); + }); + + 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); + + const apiSerializers = { + all: sinon.stub().resolves(), + posts: { + all: sinon.stub().resolves(), + browse: sinon.stub().resolves() + } + }; + + const apiConfig = {docName: 'posts', method: 'browse'}; + const frame = {}; + + const stubsToCheck = [ + 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; + + beforeEach(function () { + response = []; + apiConfig = {docName: 'posts', method: 'add'}; + frame = {}; + }); + + it('no models passed', function () { + return shared.serializers.handle.output(null, {}, {}, {}); + }); + + it('no api config passed', function () { + return shared.serializers.handle.output([]) + .then(Promise.reject) + .catch((err) => { + (err instanceof errors.IncorrectUsageError).should.be.true(); + }); + }); + + it('no api serializers passed', function () { + return shared.serializers.handle.output([], {}) + .then(Promise.reject) + .catch((err) => { + (err instanceof errors.IncorrectUsageError).should.be.true(); + }); + }); + + describe('Specific serializers only', function () { + beforeEach(function () { + apiSerializers = { + posts: { + add: sinon.stub().resolves() + }, + users: { + add: sinon.stub().resolves() + } + }; + }); + + 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.notCalled(apiSerializers.users.add); + }); + }); + + it('no serializer called if there is no match', function () { + apiConfig = {docName: 'posts', method: 'idontexist'}; + + return shared.serializers.handle.output(response, apiConfig, apiSerializers, frame) + .then(() => { + sinon.assert.notCalled(apiSerializers.posts.add); + sinon.assert.notCalled(apiSerializers.users.add); + }); + }); + }); + + describe('Custom and global (all) serializers', function () { + beforeEach(function () { + apiSerializers = { + all: { + after: sinon.stub().resolves(), + before: sinon.stub().resolves() + + }, + posts: { + add: sinon.stub().resolves(), + all: sinon.stub().resolves() + } + }; + }); + + 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) + .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.notCalled(apiSerializers.posts.all); + }); + }); + + it('calls all serializer if custom one does not exist', function () { + apiConfig = {docName: 'posts', method: 'idontexist'}; + + const stubsToCheck = [ + apiSerializers.all.before, + apiSerializers.posts.all + ]; + + 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.notCalled(apiSerializers.posts.add); + }); + }); + }); + + describe('Custom, default and global (all) serializers with no custom fallback', function () { + beforeEach(function () { + apiSerializers = { + all: { + after: sinon.stub().resolves(), + before: sinon.stub().resolves() + + }, + default: { + add: sinon.stub().resolves(), + all: sinon.stub().resolves() + + }, + posts: { + add: sinon.stub().resolves() + } + }; + }); + + 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) + .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.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'}; + + const stubsToCheck = [ + apiSerializers.all.before, + apiSerializers.default.all + ]; + + 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.notCalled(apiSerializers.posts.add); + sinon.assert.notCalled(apiSerializers.default.add); + }); + }); + }); + + describe('Custom, default and global (all) serializers with custom fallback', function () { + beforeEach(function () { + apiSerializers = { + all: { + after: sinon.stub().resolves(), + before: sinon.stub().resolves() + + }, + default: { + add: sinon.stub().resolves(), + all: sinon.stub().resolves() + + }, + posts: { + add: 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 + ]; + + 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.notCalled(apiSerializers.posts.all); + 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'}; + + const stubsToCheck = [ + apiSerializers.all.before, + apiSerializers.posts.all + ]; + + 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.notCalled(apiSerializers.posts.add); + sinon.assert.notCalled(apiSerializers.default.add); + sinon.assert.notCalled(apiSerializers.default.all); + }); + }); + }); + + describe('Default and global (all) serializers work together correctly', function () { + beforeEach(function () { + apiSerializers = { + all: { + after: sinon.stub().resolves(), + before: sinon.stub().resolves() + + }, + default: { + add: 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 + ]; + + 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.notCalled(apiSerializers.default.all); + }); + }); + + 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 + ]; + + 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.notCalled(apiSerializers.default.add); + }); + }); + }); + }); +}); diff --git a/ghost/api-framework/test/serializers/input/all.test.js b/ghost/api-framework/test/serializers/input/all.test.js new file mode 100644 index 00000000000..b24cefb9bba --- /dev/null +++ b/ghost/api-framework/test/serializers/input/all.test.js @@ -0,0 +1,81 @@ +const should = require('should'); +const shared = require('../../../'); + +describe('Unit: utils/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' + }, + options: { + include: 'tags', + fields: 'id,status', + formats: 'html', + context: {} + } + }; + + shared.serializers.input.all.all(apiConfig, frame); + + should.exist(frame.original.include); + should.exist(frame.original.fields); + should.exist(frame.original.formats); + + should.not.exist(frame.options.include); + should.not.exist(frame.options.fields); + should.exist(frame.options.formats); + should.exist(frame.options.columns); + should.exist(frame.options.withRelated); + + frame.options.withRelated.should.eql(['tags']); + frame.options.columns.should.eql(['id','status','html']); + frame.options.formats.should.eql(['html']); + }); + + describe('extra allowed internal options', function () { + it('internal access', function () { + const frame = { + options: { + context: { + internal: true + }, + transacting: true, + forUpdate: true + } + }; + + const apiConfig = {}; + + shared.serializers.input.all.all(apiConfig, frame); + + should.exist(frame.options.transacting); + should.exist(frame.options.forUpdate); + should.exist(frame.options.context); + }); + + it('no internal access', function () { + const frame = { + options: { + context: { + user: true + }, + transacting: true, + forUpdate: true + } + }; + + const apiConfig = {}; + + shared.serializers.input.all.all(apiConfig, frame); + + should.not.exist(frame.options.transacting); + should.not.exist(frame.options.forUpdate); + should.exist(frame.options.context); + }); + }); + }); +}); diff --git a/ghost/api-framework/test/util/options.test.js b/ghost/api-framework/test/util/options.test.js new file mode 100644 index 00000000000..a2d10f5d0f5 --- /dev/null +++ b/ghost/api-framework/test/util/options.test.js @@ -0,0 +1,23 @@ +const optionsUtil = require('../../lib/utils/options'); + +describe('Unit: api/shared/util/options', function () { + it('returns an array with empty string when no parameters are passed', function () { + optionsUtil.trimAndLowerCase().should.eql(['']); + }); + + it('returns single item array', function () { + optionsUtil.trimAndLowerCase('butter').should.eql(['butter']); + }); + + it('returns multiple items in array', function () { + optionsUtil.trimAndLowerCase('peanut, butter').should.eql(['peanut', 'butter']); + }); + + it('lowercases and trims items in the string', function () { + optionsUtil.trimAndLowerCase(' PeanUt, buTTer ').should.eql(['peanut', 'butter']); + }); + + it('accepts parameters in form of an array', function () { + optionsUtil.trimAndLowerCase([' PeanUt', ' buTTer ']).should.eql(['peanut', 'butter']); + }); +}); diff --git a/ghost/api-framework/test/validators/handle.test.js b/ghost/api-framework/test/validators/handle.test.js new file mode 100644 index 00000000000..bf28a5ffb09 --- /dev/null +++ b/ghost/api-framework/test/validators/handle.test.js @@ -0,0 +1,60 @@ +const errors = require('@tryghost/errors'); +const Promise = require('bluebird'); +const sinon = require('sinon'); +const shared = require('../../'); + +describe('Unit: api/shared/validators/handle', function () { + afterEach(function () { + sinon.restore(); + }); + + describe('input', function () { + it('no api config passed', function () { + return shared.validators.handle.input() + .then(Promise.reject) + .catch((err) => { + (err instanceof errors.IncorrectUsageError).should.be.true(); + }); + }); + + it('no api validators passed', function () { + return shared.validators.handle.input({}) + .then(Promise.reject) + .catch((err) => { + (err instanceof errors.IncorrectUsageError).should.be.true(); + }); + }); + + it('ensure validators are called', function () { + const getStub = sinon.stub(); + const addStub = sinon.stub(); + sinon.stub(shared.validators.input.all, 'all').get(() => { + return getStub; + }); + sinon.stub(shared.validators.input.all, 'add').get(() => { + return addStub; + }); + + const apiValidators = { + all: { + add: sinon.stub().resolves() + }, + posts: { + add: sinon.stub().resolves() + }, + users: { + add: sinon.stub().resolves() + } + }; + + return shared.validators.handle.input({docName: 'posts', method: 'add'}, apiValidators, {context: {}}) + .then(() => { + getStub.calledOnce.should.be.true(); + addStub.calledOnce.should.be.true(); + apiValidators.all.add.calledOnce.should.be.true(); + apiValidators.posts.add.calledOnce.should.be.true(); + apiValidators.users.add.called.should.be.false(); + }); + }); + }); +}); diff --git a/ghost/api-framework/test/validators/input/all.test.js b/ghost/api-framework/test/validators/input/all.test.js new file mode 100644 index 00000000000..0f9a53169c5 --- /dev/null +++ b/ghost/api-framework/test/validators/input/all.test.js @@ -0,0 +1,405 @@ +const errors = require('@tryghost/errors'); +const should = require('should'); +const sinon = require('sinon'); +const Promise = require('bluebird'); +const shared = require('../../../'); + +describe('Unit: api/shared/validators/input/all', function () { + afterEach(function () { + sinon.restore(); + }); + + describe('all', function () { + it('default', function () { + const frame = { + options: { + context: {}, + 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(() => { + should.exist(frame.options.page); + should.exist(frame.options.slug); + should.exist(frame.options.include); + should.exist(frame.options.context); + }); + }); + + it('should run global validations on an type that has validation defined', function () { + const frame = { + options: { + slug: 'not a valid slug %%%%% http://' + } + }; + + const apiConfig = { + options: { + slug: { + required: true + } + } + }; + + return shared.validators.input.all.all(apiConfig, frame) + .then(() => { + throw new Error('Should not resolve'); + }, (err) => { + should.exist(err); + }); + }); + + it('allows empty values', function () { + const frame = { + options: { + context: {}, + formats: '' + } + }; + + const apiConfig = { + options: { + formats: ['format1'] + } + }; + + return shared.validators.input.all.all(apiConfig, frame); + }); + + it('supports include being an array', function () { + const frame = { + options: { + context: {}, + 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(() => { + should.exist(frame.options.page); + should.exist(frame.options.slug); + should.exist(frame.options.include); + should.exist(frame.options.context); + }); + }); + + it('default include array notation', function () { + const frame = { + options: { + context: {}, + slug: 'slug', + include: 'tags,authors', + page: 2 + } + }; + + const apiConfig = { + options: { + include: ['tags', 'authors'] + } + }; + + return shared.validators.input.all.all(apiConfig, frame) + .then(() => { + should.exist(frame.options.page); + should.exist(frame.options.slug); + should.exist(frame.options.include); + should.exist(frame.options.context); + }); + }); + + it('does not fail', function () { + const frame = { + options: { + context: {}, + include: 'tags,authors' + } + }; + + const apiConfig = { + options: { + include: { + values: ['tags'] + } + } + }; + + return shared.validators.input.all.all(apiConfig, frame) + .then(Promise.reject) + .catch((err) => { + should.not.exist(err); + }); + }); + + it('does not fail include array notation', function () { + const frame = { + options: { + context: {}, + include: 'tags,authors' + } + }; + + const apiConfig = { + options: { + include: ['tags'] + } + }; + + return shared.validators.input.all.all(apiConfig, frame) + .then(Promise.reject) + .catch((err) => { + should.not.exist(err); + }); + }); + + it('fails', function () { + const frame = { + options: { + context: {} + } + }; + + const apiConfig = { + options: { + include: { + required: true + } + } + }; + + return shared.validators.input.all.all(apiConfig, frame) + .then(Promise.reject) + .catch((err) => { + should.exist(err); + }); + }); + + it('invalid fields', function () { + const frame = { + options: { + context: {}, + id: 'invalid' + } + }; + + const apiConfig = {}; + + return shared.validators.input.all.all(apiConfig, frame) + .then(Promise.reject) + .catch((err) => { + should.exist(err); + }); + }); + }); + + describe('browse', function () { + it('default', function () { + const frame = { + options: { + context: {} + }, + data: { + status: 'aus' + } + }; + + const apiConfig = {}; + + shared.validators.input.all.browse(apiConfig, frame); + should.exist(frame.options.context); + should.exist(frame.data.status); + }); + + it('fails', function () { + const frame = { + options: { + context: {} + }, + data: { + id: 'no-id' + } + }; + + const apiConfig = {}; + + return shared.validators.input.all.browse(apiConfig, frame) + .then(Promise.reject) + .catch((err) => { + should.exist(err); + }); + }); + }); + + describe('read', function () { + it('default', function () { + sinon.stub(shared.validators.input.all, 'browse'); + + const frame = { + options: { + context: {} + } + }; + + const apiConfig = {}; + + shared.validators.input.all.read(apiConfig, frame); + shared.validators.input.all.browse.calledOnce.should.be.true(); + }); + }); + + describe('add', function () { + it('fails', function () { + const frame = { + data: {} + }; + + const apiConfig = { + docName: 'docName' + }; + + return shared.validators.input.all.add(apiConfig, frame) + .then(Promise.reject) + .catch((err) => { + should.exist(err); + }); + }); + + it('fails with docName', function () { + const frame = { + data: { + docName: true + } + }; + + const apiConfig = { + docName: 'docName' + }; + + return shared.validators.input.all.add(apiConfig, frame) + .then(Promise.reject) + .catch((err) => { + should.exist(err); + }); + }); + + it('fails for required field', function () { + const frame = { + data: { + docName: [{ + a: 'b' + }] + } + }; + + const apiConfig = { + docName: 'docName', + data: { + b: { + required: true + } + } + }; + + return shared.validators.input.all.add(apiConfig, frame) + .then(Promise.reject) + .catch((err) => { + should.exist(err); + err.message.should.eql('Validation (FieldIsRequired) failed for ["b"]'); + }); + }); + + it('fails for invalid field', function () { + const frame = { + data: { + docName: [{ + a: 'b', + b: null + }] + } + }; + + const apiConfig = { + docName: 'docName', + data: { + b: { + required: true + } + } + }; + + return shared.validators.input.all.add(apiConfig, frame) + .then(Promise.reject) + .catch((err) => { + should.exist(err); + err.message.should.eql('Validation (FieldIsInvalid) failed for ["b"]'); + }); + }); + + it('success', function () { + const frame = { + data: { + docName: [{ + a: 'b' + }] + } + }; + + const apiConfig = { + docName: 'docName' + }; + + const result = shared.validators.input.all.add(apiConfig, frame); + (result instanceof Promise).should.not.be.true(); + }); + }); + + describe('edit', function () { + it('id mismatch', function () { + const apiConfig = { + docName: 'users' + }; + + const frame = { + options: { + id: 'zwei' + }, + data: { + posts: [ + { + id: 'eins' + } + ] + } + }; + + return shared.validators.input.all.edit(apiConfig, frame) + .then(Promise.reject) + .catch((err) => { + (err instanceof errors.BadRequestError).should.be.true(); + }); + }); + }); +}); From b10cf902a0b5a7e8b7642891ccbec5c3ec91a1cf Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Thu, 11 Aug 2022 16:42:21 +0200 Subject: [PATCH 002/118] Updated API framework debug and test names - these tests have moved from `core/` so the names are no longer relevant --- ghost/api-framework/lib/frame.js | 2 +- ghost/api-framework/lib/headers.js | 2 +- ghost/api-framework/lib/http.js | 2 +- ghost/api-framework/lib/pipeline.js | 2 +- ghost/api-framework/lib/serializers/handle.js | 2 +- ghost/api-framework/lib/serializers/input/all.js | 2 +- ghost/api-framework/lib/validators/handle.js | 2 +- ghost/api-framework/lib/validators/input/all.js | 2 +- ghost/api-framework/test/frame.test.js | 2 +- ghost/api-framework/test/headers.test.js | 2 +- ghost/api-framework/test/http.test.js | 2 +- ghost/api-framework/test/pipeline.test.js | 2 +- ghost/api-framework/test/serializers/handle.test.js | 2 +- ghost/api-framework/test/serializers/input/all.test.js | 2 +- ghost/api-framework/test/util/options.test.js | 2 +- ghost/api-framework/test/validators/handle.test.js | 2 +- ghost/api-framework/test/validators/input/all.test.js | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/ghost/api-framework/lib/frame.js b/ghost/api-framework/lib/frame.js index 46dde5d6da1..b91de8dc865 100644 --- a/ghost/api-framework/lib/frame.js +++ b/ghost/api-framework/lib/frame.js @@ -1,4 +1,4 @@ -const debug = require('@tryghost/debug')('api:shared:frame'); +const debug = require('@tryghost/debug')('frame'); const _ = require('lodash'); /** diff --git a/ghost/api-framework/lib/headers.js b/ghost/api-framework/lib/headers.js index 7147b2a1fed..fa99a9eeab0 100644 --- a/ghost/api-framework/lib/headers.js +++ b/ghost/api-framework/lib/headers.js @@ -1,5 +1,5 @@ const url = require('url'); -const debug = require('@tryghost/debug')('api:shared:headers'); +const debug = require('@tryghost/debug')('headers'); const Promise = require('bluebird'); const INVALIDATE_ALL = '/*'; diff --git a/ghost/api-framework/lib/http.js b/ghost/api-framework/lib/http.js index d47aa85bb80..67a65fd114f 100644 --- a/ghost/api-framework/lib/http.js +++ b/ghost/api-framework/lib/http.js @@ -1,5 +1,5 @@ const url = require('url'); -const debug = require('@tryghost/debug')('api:shared:http'); +const debug = require('@tryghost/debug')('http'); const Frame = require('./frame'); const headers = require('./headers'); diff --git a/ghost/api-framework/lib/pipeline.js b/ghost/api-framework/lib/pipeline.js index 14dd1365e05..f5300ae0044 100644 --- a/ghost/api-framework/lib/pipeline.js +++ b/ghost/api-framework/lib/pipeline.js @@ -1,4 +1,4 @@ -const debug = require('@tryghost/debug')('api:shared:pipeline'); +const debug = require('@tryghost/debug')('pipeline'); const Promise = require('bluebird'); const _ = require('lodash'); const errors = require('@tryghost/errors'); diff --git a/ghost/api-framework/lib/serializers/handle.js b/ghost/api-framework/lib/serializers/handle.js index 39afa020ae3..84766a72ec4 100644 --- a/ghost/api-framework/lib/serializers/handle.js +++ b/ghost/api-framework/lib/serializers/handle.js @@ -1,4 +1,4 @@ -const debug = require('@tryghost/debug')('api:shared:serializers:handle'); +const debug = require('@tryghost/debug')('serializers:handle'); const Promise = require('bluebird'); const {sequence} = require('@tryghost/promise'); const errors = require('@tryghost/errors'); diff --git a/ghost/api-framework/lib/serializers/input/all.js b/ghost/api-framework/lib/serializers/input/all.js index a372dae2794..4a2a0ddad4a 100644 --- a/ghost/api-framework/lib/serializers/input/all.js +++ b/ghost/api-framework/lib/serializers/input/all.js @@ -1,4 +1,4 @@ -const debug = require('@tryghost/debug')('api:shared:serializers:input:all'); +const debug = require('@tryghost/debug')('serializers:input:all'); const _ = require('lodash'); const utils = require('../../utils'); diff --git a/ghost/api-framework/lib/validators/handle.js b/ghost/api-framework/lib/validators/handle.js index 27eb0da88ba..670106c7ee0 100644 --- a/ghost/api-framework/lib/validators/handle.js +++ b/ghost/api-framework/lib/validators/handle.js @@ -1,4 +1,4 @@ -const debug = require('@tryghost/debug')('api:shared:validators:handle'); +const debug = require('@tryghost/debug')('validators:handle'); const Promise = require('bluebird'); const errors = require('@tryghost/errors'); const {sequence} = require('@tryghost/promise'); diff --git a/ghost/api-framework/lib/validators/input/all.js b/ghost/api-framework/lib/validators/input/all.js index f6061d1c471..f2769c0b3de 100644 --- a/ghost/api-framework/lib/validators/input/all.js +++ b/ghost/api-framework/lib/validators/input/all.js @@ -1,4 +1,4 @@ -const debug = require('@tryghost/debug')('api:shared:validators:input:all'); +const debug = require('@tryghost/debug')('validators:input:all'); const _ = require('lodash'); const Promise = require('bluebird'); const tpl = require('@tryghost/tpl'); diff --git a/ghost/api-framework/test/frame.test.js b/ghost/api-framework/test/frame.test.js index bdbd645aa26..36e134497fd 100644 --- a/ghost/api-framework/test/frame.test.js +++ b/ghost/api-framework/test/frame.test.js @@ -1,7 +1,7 @@ const should = require('should'); const shared = require('../'); -describe('Unit: api/shared/frame', function () { +describe('Frame', function () { it('constructor', function () { const frame = new shared.Frame(); Object.keys(frame).should.eql([ diff --git a/ghost/api-framework/test/headers.test.js b/ghost/api-framework/test/headers.test.js index ecdbf5c2d33..67336932b43 100644 --- a/ghost/api-framework/test/headers.test.js +++ b/ghost/api-framework/test/headers.test.js @@ -1,6 +1,6 @@ const shared = require('../'); -describe('Unit: api/shared/headers', function () { +describe('Headers', function () { it('empty headers config', function () { return shared.headers.get().then((result) => { result.should.eql({}); diff --git a/ghost/api-framework/test/http.test.js b/ghost/api-framework/test/http.test.js index 7de8495fde8..bfb8b86b43e 100644 --- a/ghost/api-framework/test/http.test.js +++ b/ghost/api-framework/test/http.test.js @@ -2,7 +2,7 @@ const should = require('should'); const sinon = require('sinon'); const shared = require('../'); -describe('Unit: api/shared/http', function () { +describe('HTTP', function () { let req; let res; let next; diff --git a/ghost/api-framework/test/pipeline.test.js b/ghost/api-framework/test/pipeline.test.js index 4f139d864e2..0f6786cb7d9 100644 --- a/ghost/api-framework/test/pipeline.test.js +++ b/ghost/api-framework/test/pipeline.test.js @@ -3,7 +3,7 @@ const should = require('should'); const sinon = require('sinon'); const shared = require('../'); -describe('Unit: api/shared/pipeline', function () { +describe('Pipeline', function () { afterEach(function () { sinon.restore(); }); diff --git a/ghost/api-framework/test/serializers/handle.test.js b/ghost/api-framework/test/serializers/handle.test.js index c00b364132f..5c7344b1173 100644 --- a/ghost/api-framework/test/serializers/handle.test.js +++ b/ghost/api-framework/test/serializers/handle.test.js @@ -3,7 +3,7 @@ const Promise = require('bluebird'); const sinon = require('sinon'); const shared = require('../../'); -describe('Unit: api/shared/serializers/handle', function () { +describe('serializers/handle', function () { afterEach(function () { sinon.restore(); }); diff --git a/ghost/api-framework/test/serializers/input/all.test.js b/ghost/api-framework/test/serializers/input/all.test.js index b24cefb9bba..8148f280744 100644 --- a/ghost/api-framework/test/serializers/input/all.test.js +++ b/ghost/api-framework/test/serializers/input/all.test.js @@ -1,7 +1,7 @@ const should = require('should'); const shared = require('../../../'); -describe('Unit: utils/serializers/input/all', function () { +describe('serializers/input/all', function () { describe('all', function () { it('transforms into model readable format', function () { const apiConfig = {}; diff --git a/ghost/api-framework/test/util/options.test.js b/ghost/api-framework/test/util/options.test.js index a2d10f5d0f5..d9812a41cee 100644 --- a/ghost/api-framework/test/util/options.test.js +++ b/ghost/api-framework/test/util/options.test.js @@ -1,6 +1,6 @@ const optionsUtil = require('../../lib/utils/options'); -describe('Unit: api/shared/util/options', function () { +describe('util/options', function () { it('returns an array with empty string when no parameters are passed', function () { optionsUtil.trimAndLowerCase().should.eql(['']); }); diff --git a/ghost/api-framework/test/validators/handle.test.js b/ghost/api-framework/test/validators/handle.test.js index bf28a5ffb09..fa3df9a8996 100644 --- a/ghost/api-framework/test/validators/handle.test.js +++ b/ghost/api-framework/test/validators/handle.test.js @@ -3,7 +3,7 @@ const Promise = require('bluebird'); const sinon = require('sinon'); const shared = require('../../'); -describe('Unit: api/shared/validators/handle', function () { +describe('validators/handle', function () { afterEach(function () { sinon.restore(); }); diff --git a/ghost/api-framework/test/validators/input/all.test.js b/ghost/api-framework/test/validators/input/all.test.js index 0f9a53169c5..bd54c60bca5 100644 --- a/ghost/api-framework/test/validators/input/all.test.js +++ b/ghost/api-framework/test/validators/input/all.test.js @@ -4,7 +4,7 @@ const sinon = require('sinon'); const Promise = require('bluebird'); const shared = require('../../../'); -describe('Unit: api/shared/validators/input/all', function () { +describe('validators/input/all', function () { afterEach(function () { sinon.restore(); }); From b98b4c2b74c9e61d04eca1f93230530a505829bf Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Thu, 11 Aug 2022 17:29:08 +0200 Subject: [PATCH 003/118] Moved API documentation to api-framework README - it's better suited here given this package is now the API framework --- ghost/api-framework/README.md | 131 +++++++++++++++++++++++++++++++++- 1 file changed, 129 insertions(+), 2 deletions(-) diff --git a/ghost/api-framework/README.md b/ghost/api-framework/README.md index 64191b307a0..9d31c3939cc 100644 --- a/ghost/api-framework/README.md +++ b/ghost/api-framework/README.md @@ -1,10 +1,137 @@ -# Api Framework +# API Framework API framework used by Ghost - ## Usage +### Stages + +Each request goes through the following stages: + +- input validation +- input serialisation +- permissions +- query +- output serialisation + +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. +Each function can modify the original instance. No need to return the class instance. + +#### Structure + +``` +{ + original: Object, + options: Object, + data: Object, + user: Object, + file: Object, + files: Array +} +``` + +#### Example + +``` +{ + original: { + include: 'tags' + }, + options: { + withRelated: ['tags'] + }, + data: { + posts: [] + } +} +``` + +### API Controller + +A controller is no longer just a function, it's a set of configurations. + +#### Structure + +``` +edit: function || object +``` + +``` +edit: { + headers: object, + options: Array, + data: Array, + validation: object | function, + permissions: boolean | object | function, + query: function +} +``` + +#### Examples + + +``` +edit: { + headers: { + cacheInvalidate: true + }, + // Allowed url/query params + options: ['include'] + // Url/query param validation configuration + validation: { + options: { + include: { + required: true, + values: ['tags'] + } + } + }, + permissions: true, + // Returns a model response! + query(frame) { + return models.Post.edit(frame.data, frame.options); + } +} +``` + +``` +read: { + // Allowed url/query params, which will be remembered inside `frame.data` + // This is helpful for READ requests e.g. `model.findOne(frame.data, frame.options)`. + // Our model layer requires sending the where clauses as first parameter. + data: ['slug'] + validation: { + data: { + slug: { + values: ['eins'] + } + } + }, + permissions: true, + query(frame) { + return models.Post.findOne(frame.data, frame.options); + } +} +``` + +``` +edit: { + validation() { + // custom validation, skip framework + }, + permissions: { + unsafeAttrs: ['author'] + }, + query(frame) { + return models.Post.edit(frame.data, frame.options); + } +} +``` ## Develop From dfdcb98eb7848ea8aa87688f3f008d4b31cfd784 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Fri, 12 Aug 2022 08:12:35 +0200 Subject: [PATCH 004/118] Lazily executed calculating headers in API framework - if the API controller endpoint is a function, we early return as we expect the function to handle the response but we still ended up calculating the headers beforehand, only to be thrown away - this commit moves the header fetching code down in the flow so it's only executed when needed - this doesn't really have a big effect for us because 99% of our controllers follow the object pattern --- ghost/api-framework/lib/http.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/lib/http.js b/ghost/api-framework/lib/http.js index 67a65fd114f..ddcf7d84059 100644 --- a/ghost/api-framework/lib/http.js +++ b/ghost/api-framework/lib/http.js @@ -64,7 +64,6 @@ const http = (apiImpl) => { const result = await apiImpl(frame); debug(`External API request to ${frame.docName}.${frame.method}`); - const apiHeaders = await headers.get(result, apiImpl.headers, frame) || {}; // CASE: api ctrl wants to handle the express response (e.g. streams) if (typeof result === 'function') { @@ -82,6 +81,7 @@ const http = (apiImpl) => { res.status(statusCode); // CASE: generate headers based on the api ctrl configuration + const apiHeaders = await headers.get(result, apiImpl.headers, frame) || {}; res.set(apiHeaders); const send = (format) => { From 358c29bc5a00ee7b6659b147078db0c621d7afcd Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Fri, 12 Aug 2022 08:34:20 +0200 Subject: [PATCH 005/118] Added tests to HTTP module of api-framework - this file was mostly just missing tests for the other content disposition types, which are easily added - bumps coverage of this file to 100% --- ghost/api-framework/test/headers.test.js | 67 ++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/ghost/api-framework/test/headers.test.js b/ghost/api-framework/test/headers.test.js index 67336932b43..047e92cf7e3 100644 --- a/ghost/api-framework/test/headers.test.js +++ b/ghost/api-framework/test/headers.test.js @@ -29,6 +29,46 @@ describe('Headers', function () { }); }); + 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; + } + } + }); + result.should.eql({ + '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'}}); + result.should.eql({ + '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; + } + } + }); + result.should.eql({ + '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'}}) .then((result) => { @@ -90,6 +130,33 @@ describe('Headers', function () { }); }); + it('respects HTTP redirects', async function () { + const apiResult = { + posts: [{ + id: 'id_value' + }] + }; + + const apiConfigHeaders = {}; + const frame = { + docName: 'posts', + method: 'add', + original: { + url: { + host: 'example.com', + pathname: `/api/content/posts/`, + secure: false + } + } + }; + + const result = await shared.headers.get(apiResult, apiConfigHeaders, frame); + result.should.eql({ + // NOTE: the backslash in the end is important to avoid unecessary 301s using the header + 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 () { const apiResult = { posts: [{ From 86c6340cda83ab25df1c19afd2081f2699a46bf7 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Fri, 12 Aug 2022 08:46:32 +0200 Subject: [PATCH 006/118] Refactored api-framework to use optional chaining - this makes the code more readable and succinct --- ghost/api-framework/lib/headers.js | 8 ++------ ghost/api-framework/lib/http.js | 2 +- ghost/api-framework/lib/serializers/handle.js | 8 ++++---- ghost/api-framework/lib/validators/input/all.js | 2 +- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/ghost/api-framework/lib/headers.js b/ghost/api-framework/lib/headers.js index fa99a9eeab0..bd552059e2a 100644 --- a/ghost/api-framework/lib/headers.js +++ b/ghost/api-framework/lib/headers.js @@ -122,12 +122,8 @@ module.exports = { } } - const locationHeaderDisabled = apiConfigHeaders && apiConfigHeaders.location === false; - const hasFrameData = frame - && (frame.method === 'add') - && result[frame.docName] - && result[frame.docName][0] - && result[frame.docName][0].id; + const locationHeaderDisabled = apiConfigHeaders?.location === false; + const hasFrameData = frame?.method === 'add' && result[frame.docName]?.[0]?.id; if (!locationHeaderDisabled && hasFrameData) { const protocol = (frame.original.url.secure === false) ? 'http://' : 'https://'; diff --git a/ghost/api-framework/lib/http.js b/ghost/api-framework/lib/http.js index ddcf7d84059..72848e19a13 100644 --- a/ghost/api-framework/lib/http.js +++ b/ghost/api-framework/lib/http.js @@ -30,7 +30,7 @@ const http = (apiImpl) => { }; } - if (req.user && req.user.id) { + if (req.user?.id) { user = req.user.id; } diff --git a/ghost/api-framework/lib/serializers/handle.js b/ghost/api-framework/lib/serializers/handle.js index 84766a72ec4..962f949799a 100644 --- a/ghost/api-framework/lib/serializers/handle.js +++ b/ghost/api-framework/lib/serializers/handle.js @@ -68,10 +68,10 @@ module.exports.input = (apiConfig, apiSerializers, frame) => { }; const getBestMatchSerializer = function (apiSerializers, docName, method) { - if (apiSerializers[docName] && apiSerializers[docName][method]) { + if (apiSerializers[docName]?.[method]) { debug(`Calling ${docName}.${method}`); return apiSerializers[docName][method].bind(apiSerializers[docName]); - } else if (apiSerializers[docName] && apiSerializers[docName].all) { + } else if (apiSerializers[docName]?.all) { debug(`Calling ${docName}.all`); return apiSerializers[docName].all.bind(apiSerializers[docName]); } @@ -108,7 +108,7 @@ module.exports.output = (response = {}, apiConfig, apiSerializers, frame) => { // ##### API VERSION RESOURCE SERIALIZATION - if (apiSerializers.all && apiSerializers.all.before) { + if (apiSerializers.all?.before) { tasks.push(function allSerializeBefore() { return apiSerializers.all.before(response, apiConfig, frame); }); @@ -129,7 +129,7 @@ module.exports.output = (response = {}, apiConfig, apiSerializers, frame) => { }); } - if (apiSerializers.all && apiSerializers.all.after) { + if (apiSerializers.all?.after) { tasks.push(function allSerializeAfter() { return apiSerializers.all.after(apiConfig, frame); }); diff --git a/ghost/api-framework/lib/validators/input/all.js b/ghost/api-framework/lib/validators/input/all.js index f2769c0b3de..c6a8709408a 100644 --- a/ghost/api-framework/lib/validators/input/all.js +++ b/ghost/api-framework/lib/validators/input/all.js @@ -53,7 +53,7 @@ const validate = (config, attrs) => { errors = errors.concat(validator.validate(value, key, GLOBAL_VALIDATORS[key])); } - if (config && config[key]) { + if (config?.[key]) { const allowedValues = Array.isArray(config[key]) ? config[key] : config[key].values; if (allowedValues) { From 801a04a2f6c2dadd25af6a9ea618da99e28f5a63 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Mon, 15 Aug 2022 15:20:45 +0200 Subject: [PATCH 007/118] Fixed full Admin test suite running during unit tests - because of how the npm scripts were set up, we were running the full Admin integration tests during the unit tests phase of CI - this commit renames the majority of `test` to `test:unit` in the package.json files, and aliases `test` to `test:unit` - special packages like Admin have no-op'd `test:unit` scripts so we don't end up running its tests --- ghost/api-framework/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 585a9421061..9c9baceffc6 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -7,7 +7,8 @@ "main": "index.js", "scripts": { "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing c8 --all --reporter text --reporter cobertura mocha './test/**/*.test.js'", + "test:unit": "NODE_ENV=testing c8 --all --reporter text --reporter cobertura mocha './test/**/*.test.js'", + "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" From 4386ac1e66a1b3e3fab188ae2d802bf10c788975 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Thu, 18 Aug 2022 11:36:16 +0200 Subject: [PATCH 008/118] Organized package dependencies - cleaned up unused dependencies - adds missing dependencies that are used in the code - this should help us be more explicit about the dependencies a package uses --- ghost/api-framework/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 9c9baceffc6..1601ffd9b0f 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -18,6 +18,7 @@ "lib" ], "devDependencies": { + "bluebird": "3.7.2", "c8": "7.12.0", "mocha": "10.0.0", "should": "13.2.3", From 5c50c56b90177d13c76945ba47cf8dc8fd8d6f99 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Sun, 21 Aug 2022 16:31:41 +0100 Subject: [PATCH 009/118] Fixed some type issues with the api framework - fixes a bunch of red squiggly lines due to type issues - this in turn makes it slightly easier to read the API pipeline code --- ghost/api-framework/lib/frame.js | 5 +++++ ghost/api-framework/lib/pipeline.js | 16 +++++++--------- ghost/api-framework/test/frame.test.js | 5 ++++- ghost/api-framework/test/http.test.js | 5 ++++- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/ghost/api-framework/lib/frame.js b/ghost/api-framework/lib/frame.js index b91de8dc865..2be8cb3b747 100644 --- a/ghost/api-framework/lib/frame.js +++ b/ghost/api-framework/lib/frame.js @@ -19,6 +19,8 @@ class Frame { * file: Uploaded file * files: Uploaded files * apiType: Content or admin api access + * docName: The endpoint name, e.g. "posts" + * method: The method name, e.g. "browse" */ this.options = {}; this.data = {}; @@ -26,6 +28,9 @@ class Frame { this.file = {}; this.files = []; this.apiType = null; + this.docName = null; + this.method = null; + this.response = null; } /** diff --git a/ghost/api-framework/lib/pipeline.js b/ghost/api-framework/lib/pipeline.js index f5300ae0044..81f2120dd79 100644 --- a/ghost/api-framework/lib/pipeline.js +++ b/ghost/api-framework/lib/pipeline.js @@ -174,23 +174,21 @@ const STAGES = { * 4. Controller - Execute the controller implementation & receive model response. * 5. Output Serialisation - Output formatting, Deprecations, Extra attributes etc... * - * @param {Function} apiController + * @param {Object} apiController * @param {Object} apiUtils - Local utils (validation & serialisation) from target API version * @param {String} [apiType] - Content or Admin API access - * @return {Function} + * @return {Object} */ const pipeline = (apiController, apiUtils, apiType) => { const keys = Object.keys(apiController); + const docName = apiController.docName; // CASE: api controllers are objects with configuration. // We have to ensure that we expose a functional interface e.g. `api.posts.add` has to be available. - return keys.reduce((obj, key) => { - const docName = apiController.docName; - const method = key; + return keys.reduce((obj, method) => { + const apiImpl = _.cloneDeep(apiController)[method]; - const apiImpl = _.cloneDeep(apiController)[key]; - - obj[key] = function wrapper() { + obj[method] = function wrapper() { const apiConfig = {docName, method}; let options; let data; @@ -253,7 +251,7 @@ const pipeline = (apiController, apiUtils, apiType) => { }); }; - Object.assign(obj[key], apiImpl); + Object.assign(obj[method], apiImpl); return obj; }, {}); }; diff --git a/ghost/api-framework/test/frame.test.js b/ghost/api-framework/test/frame.test.js index 36e134497fd..c53eb2dc1c8 100644 --- a/ghost/api-framework/test/frame.test.js +++ b/ghost/api-framework/test/frame.test.js @@ -11,7 +11,10 @@ describe('Frame', function () { 'user', 'file', 'files', - 'apiType' + 'apiType', + 'docName', + 'method', + 'response' ]); }); diff --git a/ghost/api-framework/test/http.test.js b/ghost/api-framework/test/http.test.js index bfb8b86b43e..ad7a0381612 100644 --- a/ghost/api-framework/test/http.test.js +++ b/ghost/api-framework/test/http.test.js @@ -45,7 +45,10 @@ describe('HTTP', function () { 'user', 'file', 'files', - 'apiType' + 'apiType', + 'docName', + 'method', + 'response' ]); apiImpl.args[0][0].data.should.eql({a: 'a'}); From 10563cfe3cdcbb64fecd7bc3c069184356aa5033 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 Sep 2022 12:09:33 +0100 Subject: [PATCH 010/118] Updated @tryghost dependencies (#15404) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- ghost/api-framework/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 1601ffd9b0f..d5d8107a361 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -26,10 +26,10 @@ }, "dependencies": { "@tryghost/debug": "0.1.18", - "@tryghost/errors": "1.2.15", + "@tryghost/errors": "1.2.16", "@tryghost/promise": "0.1.21", "@tryghost/tpl": "0.1.18", - "@tryghost/validator": "0.1.27", + "@tryghost/validator": "0.1.28", "jsonpath": "1.1.1", "lodash": "4.17.21" } From 767f080d3a8aa341834a9f8667d00ebd3efff57c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Sep 2022 08:31:35 +0700 Subject: [PATCH 011/118] Updated @tryghost dependencies (#15434) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- ghost/api-framework/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index d5d8107a361..41a1e19d319 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -26,10 +26,10 @@ }, "dependencies": { "@tryghost/debug": "0.1.18", - "@tryghost/errors": "1.2.16", + "@tryghost/errors": "1.2.17", "@tryghost/promise": "0.1.21", "@tryghost/tpl": "0.1.18", - "@tryghost/validator": "0.1.28", + "@tryghost/validator": "0.1.29", "jsonpath": "1.1.1", "lodash": "4.17.21" } From 1f11a6ec8cb843c4d45a6fe62b827e20b4875767 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Oct 2022 00:36:08 +0000 Subject: [PATCH 012/118] Update Test & linting packages --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 41a1e19d319..3eb44313828 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -22,7 +22,7 @@ "c8": "7.12.0", "mocha": "10.0.0", "should": "13.2.3", - "sinon": "14.0.0" + "sinon": "14.0.1" }, "dependencies": { "@tryghost/debug": "0.1.18", From e645796571d34e46bcef8b228b9911d10063a3d7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Oct 2022 10:16:05 +0700 Subject: [PATCH 013/118] Updated @tryghost dependencies (#15479) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- ghost/api-framework/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 3eb44313828..9b71a61a996 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -26,8 +26,8 @@ }, "dependencies": { "@tryghost/debug": "0.1.18", - "@tryghost/errors": "1.2.17", - "@tryghost/promise": "0.1.21", + "@tryghost/errors": "1.2.18", + "@tryghost/promise": "0.1.22", "@tryghost/tpl": "0.1.18", "@tryghost/validator": "0.1.29", "jsonpath": "1.1.1", From 1d3495ed4d2dd3a1c0dff140b0cca614d00d3764 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 08:41:28 +0000 Subject: [PATCH 014/118] Update dependency mocha to v10.1.0 --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 9b71a61a996..4dc23c6d411 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -20,7 +20,7 @@ "devDependencies": { "bluebird": "3.7.2", "c8": "7.12.0", - "mocha": "10.0.0", + "mocha": "10.1.0", "should": "13.2.3", "sinon": "14.0.1" }, From b1c2fae1d00a166a1c742054941293166765ed2f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Oct 2022 10:30:40 +0700 Subject: [PATCH 015/118] Updated @tryghost dependencies (#15631) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- ghost/api-framework/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 4dc23c6d411..5fd5e69b181 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -25,10 +25,10 @@ "sinon": "14.0.1" }, "dependencies": { - "@tryghost/debug": "0.1.18", + "@tryghost/debug": "0.1.19", "@tryghost/errors": "1.2.18", "@tryghost/promise": "0.1.22", - "@tryghost/tpl": "0.1.18", + "@tryghost/tpl": "0.1.19", "@tryghost/validator": "0.1.29", "jsonpath": "1.1.1", "lodash": "4.17.21" From 6760abf8bed96ac9996574a6f0d9b5c930011795 Mon Sep 17 00:00:00 2001 From: Halldor Thorhallsson Date: Mon, 31 Oct 2022 15:30:18 -0400 Subject: [PATCH 016/118] Removed bluebird from `api-framework` module (#15685) refs: https://github.com/TryGhost/Ghost/issues/14882 - Removing bluebird specific methods in favour of the Ghost sequence method so we can remove the bluebird dependency --- ghost/api-framework/lib/headers.js | 1 - ghost/api-framework/lib/pipeline.js | 1 - ghost/api-framework/lib/serializers/handle.js | 1 - ghost/api-framework/lib/validators/handle.js | 1 - ghost/api-framework/lib/validators/input/all.js | 1 - ghost/api-framework/package.json | 1 - ghost/api-framework/test/serializers/handle.test.js | 1 - ghost/api-framework/test/validators/handle.test.js | 1 - ghost/api-framework/test/validators/input/all.test.js | 5 ++--- 9 files changed, 2 insertions(+), 11 deletions(-) diff --git a/ghost/api-framework/lib/headers.js b/ghost/api-framework/lib/headers.js index bd552059e2a..81d9b75d18f 100644 --- a/ghost/api-framework/lib/headers.js +++ b/ghost/api-framework/lib/headers.js @@ -1,6 +1,5 @@ const url = require('url'); const debug = require('@tryghost/debug')('headers'); -const Promise = require('bluebird'); const INVALIDATE_ALL = '/*'; const cacheInvalidate = (result, options = {}) => { diff --git a/ghost/api-framework/lib/pipeline.js b/ghost/api-framework/lib/pipeline.js index 81f2120dd79..45856281f51 100644 --- a/ghost/api-framework/lib/pipeline.js +++ b/ghost/api-framework/lib/pipeline.js @@ -1,5 +1,4 @@ const debug = require('@tryghost/debug')('pipeline'); -const Promise = require('bluebird'); const _ = require('lodash'); const errors = require('@tryghost/errors'); const {sequence} = require('@tryghost/promise'); diff --git a/ghost/api-framework/lib/serializers/handle.js b/ghost/api-framework/lib/serializers/handle.js index 962f949799a..c86fe1bdfaa 100644 --- a/ghost/api-framework/lib/serializers/handle.js +++ b/ghost/api-framework/lib/serializers/handle.js @@ -1,5 +1,4 @@ const debug = require('@tryghost/debug')('serializers:handle'); -const Promise = require('bluebird'); const {sequence} = require('@tryghost/promise'); const errors = require('@tryghost/errors'); diff --git a/ghost/api-framework/lib/validators/handle.js b/ghost/api-framework/lib/validators/handle.js index 670106c7ee0..ab9901ea9f0 100644 --- a/ghost/api-framework/lib/validators/handle.js +++ b/ghost/api-framework/lib/validators/handle.js @@ -1,5 +1,4 @@ const debug = require('@tryghost/debug')('validators:handle'); -const Promise = require('bluebird'); const errors = require('@tryghost/errors'); const {sequence} = require('@tryghost/promise'); diff --git a/ghost/api-framework/lib/validators/input/all.js b/ghost/api-framework/lib/validators/input/all.js index c6a8709408a..f6e4efc664b 100644 --- a/ghost/api-framework/lib/validators/input/all.js +++ b/ghost/api-framework/lib/validators/input/all.js @@ -1,6 +1,5 @@ const debug = require('@tryghost/debug')('validators:input:all'); const _ = require('lodash'); -const Promise = require('bluebird'); const tpl = require('@tryghost/tpl'); const {BadRequestError, ValidationError} = require('@tryghost/errors'); const validator = require('@tryghost/validator'); diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 5fd5e69b181..8ffa2eca859 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -18,7 +18,6 @@ "lib" ], "devDependencies": { - "bluebird": "3.7.2", "c8": "7.12.0", "mocha": "10.1.0", "should": "13.2.3", diff --git a/ghost/api-framework/test/serializers/handle.test.js b/ghost/api-framework/test/serializers/handle.test.js index 5c7344b1173..24612ac85c0 100644 --- a/ghost/api-framework/test/serializers/handle.test.js +++ b/ghost/api-framework/test/serializers/handle.test.js @@ -1,5 +1,4 @@ const errors = require('@tryghost/errors'); -const Promise = require('bluebird'); const sinon = require('sinon'); const shared = require('../../'); diff --git a/ghost/api-framework/test/validators/handle.test.js b/ghost/api-framework/test/validators/handle.test.js index fa3df9a8996..da8bf5dad84 100644 --- a/ghost/api-framework/test/validators/handle.test.js +++ b/ghost/api-framework/test/validators/handle.test.js @@ -1,5 +1,4 @@ const errors = require('@tryghost/errors'); -const Promise = require('bluebird'); const sinon = require('sinon'); const shared = require('../../'); diff --git a/ghost/api-framework/test/validators/input/all.test.js b/ghost/api-framework/test/validators/input/all.test.js index bd54c60bca5..c35a9da3d78 100644 --- a/ghost/api-framework/test/validators/input/all.test.js +++ b/ghost/api-framework/test/validators/input/all.test.js @@ -1,7 +1,6 @@ const errors = require('@tryghost/errors'); const should = require('should'); const sinon = require('sinon'); -const Promise = require('bluebird'); const shared = require('../../../'); describe('validators/input/all', function () { @@ -148,7 +147,7 @@ describe('validators/input/all', function () { }; return shared.validators.input.all.all(apiConfig, frame) - .then(Promise.reject) + .then(Promise.reject.bind(Promise)) .catch((err) => { should.not.exist(err); }); @@ -169,7 +168,7 @@ describe('validators/input/all', function () { }; return shared.validators.input.all.all(apiConfig, frame) - .then(Promise.reject) + .then(Promise.reject.bind(Promise)) .catch((err) => { should.not.exist(err); }); From 59f0397ba7604b14c86972fb41ec4861385911ad Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Nov 2022 20:39:48 +0000 Subject: [PATCH 017/118] Update Test & linting packages --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 8ffa2eca859..dbf2cb32d41 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -21,7 +21,7 @@ "c8": "7.12.0", "mocha": "10.1.0", "should": "13.2.3", - "sinon": "14.0.1" + "sinon": "14.0.2" }, "dependencies": { "@tryghost/debug": "0.1.19", From 2c68b740dde1b4e28ce79bfcfd12fa08c15593c0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 Dec 2022 13:20:22 +0000 Subject: [PATCH 018/118] Update dependency mocha to v10.2.0 --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index dbf2cb32d41..92023769032 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -19,7 +19,7 @@ ], "devDependencies": { "c8": "7.12.0", - "mocha": "10.1.0", + "mocha": "10.2.0", "should": "13.2.3", "sinon": "14.0.2" }, From 0a254556a05dba3a91a5b23229e91218468f837f Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Wed, 14 Dec 2022 11:18:55 +0700 Subject: [PATCH 019/118] Updated @tryghost dependencies (#16005) - also includes `knex-migrator` with a simple `sqlite3` bump --- ghost/api-framework/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 92023769032..ad37f43c355 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -24,11 +24,11 @@ "sinon": "14.0.2" }, "dependencies": { - "@tryghost/debug": "0.1.19", - "@tryghost/errors": "1.2.18", + "@tryghost/debug": "0.1.20", + "@tryghost/errors": "1.2.19", "@tryghost/promise": "0.1.22", - "@tryghost/tpl": "0.1.19", - "@tryghost/validator": "0.1.29", + "@tryghost/tpl": "0.1.20", + "@tryghost/validator": "0.1.31", "jsonpath": "1.1.1", "lodash": "4.17.21" } From 807973ed9e7e287ab2422b1f523c837b5d46080f Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Mon, 2 Jan 2023 19:32:59 +0100 Subject: [PATCH 020/118] Bumped TryGhost-owned dependencies and lockfile - this was all getting terribly behind so I've done several things: - majority of `@tryghost/*` except Lexical packages - gscan + knex-migrator to remove old `@tryghost/errors` usage - bumped lockfile --- ghost/api-framework/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index ad37f43c355..5e0de7d3bc2 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -24,10 +24,10 @@ "sinon": "14.0.2" }, "dependencies": { - "@tryghost/debug": "0.1.20", - "@tryghost/errors": "1.2.19", + "@tryghost/debug": "0.1.21", + "@tryghost/errors": "1.2.20", "@tryghost/promise": "0.1.22", - "@tryghost/tpl": "0.1.20", + "@tryghost/tpl": "0.1.21", "@tryghost/validator": "0.1.31", "jsonpath": "1.1.1", "lodash": "4.17.21" From 8be7906569585949ab14410bc87dbdb1166fd7d4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Feb 2023 22:15:50 +0000 Subject: [PATCH 021/118] Update dependency c8 to v7.13.0 --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 5e0de7d3bc2..dca54809200 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -18,7 +18,7 @@ "lib" ], "devDependencies": { - "c8": "7.12.0", + "c8": "7.13.0", "mocha": "10.2.0", "should": "13.2.3", "sinon": "14.0.2" From 6157cc30c3cdc2703c22b8b6ab877866114eaa99 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Wed, 22 Feb 2023 11:32:11 +0100 Subject: [PATCH 022/118] Updated `@tryghost/errors` dependency - there's a weird situation when we have mixed versions of the dependency because different libraries try to compare instances - this brings the usage up to 1.2.21 so we can fix the build for now --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index dca54809200..5ff94957bda 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@tryghost/debug": "0.1.21", - "@tryghost/errors": "1.2.20", + "@tryghost/errors": "1.2.21", "@tryghost/promise": "0.1.22", "@tryghost/tpl": "0.1.21", "@tryghost/validator": "0.1.31", From 5889c477b8b48a6940efb06470b6fb6771c6f98b Mon Sep 17 00:00:00 2001 From: Naz Date: Wed, 22 Feb 2023 15:17:39 +0800 Subject: [PATCH 023/118] Added cache support to api-framework refs https://github.com/TryGhost/Toolbox/issues/522 - API-level response caching allows to cache responses bypassing the "pipeline" processing - The main usecase for these caches is caching GET requests for expensive Content API requests - To enable response caching add a "cache" key with a cache instance as a value, for example for posts public cache configuration can look like: ``` module.exports = { docName: 'posts', browse: { cache: postsPublicService.api.cache, options: [ ... ``` --- ghost/api-framework/lib/pipeline.js | 16 ++++- ghost/api-framework/test/pipeline.test.js | 83 +++++++++++++++++++++++ 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/ghost/api-framework/lib/pipeline.js b/ghost/api-framework/lib/pipeline.js index 45856281f51..5d6e1bbb0ac 100644 --- a/ghost/api-framework/lib/pipeline.js +++ b/ghost/api-framework/lib/pipeline.js @@ -187,7 +187,7 @@ const pipeline = (apiController, apiUtils, apiType) => { return keys.reduce((obj, method) => { const apiImpl = _.cloneDeep(apiController)[method]; - obj[method] = function wrapper() { + obj[method] = async function wrapper() { const apiConfig = {docName, method}; let options; let data; @@ -229,6 +229,15 @@ const pipeline = (apiController, apiUtils, apiType) => { frame.docName = docName; frame.method = method; + let cacheKey = JSON.stringify(frame.options); + if (apiImpl.cache) { + const response = await apiImpl.cache.get(cacheKey); + + if (response) { + return Promise.resolve(response); + } + } + return Promise.resolve() .then(() => { return STAGES.validation.input(apiUtils, apiConfig, apiImpl, frame); @@ -245,7 +254,10 @@ const pipeline = (apiController, apiUtils, apiType) => { .then((response) => { return STAGES.serialisation.output(response, apiUtils, apiConfig, apiImpl, frame); }) - .then(() => { + .then(async () => { + if (apiImpl.cache) { + await apiImpl.cache.set(cacheKey, frame.response); + } return frame.response; }); }; diff --git a/ghost/api-framework/test/pipeline.test.js b/ghost/api-framework/test/pipeline.test.js index 0f6786cb7d9..3e57081eaad 100644 --- a/ghost/api-framework/test/pipeline.test.js +++ b/ghost/api-framework/test/pipeline.test.js @@ -250,4 +250,87 @@ describe('Pipeline', 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'); + }); + + 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) + } + } + }; + + const apiUtils = {}; + const result = shared.pipeline(apiController, apiUtils); + + 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; + }); + + const response = await result.browse(); + + response.should.eql('response'); + + // request went through all stages + shared.pipeline.STAGES.validation.input.calledOnce.should.be.true(); + shared.pipeline.STAGES.serialisation.input.calledOnce.should.be.true(); + shared.pipeline.STAGES.permissions.calledOnce.should.be.true(); + shared.pipeline.STAGES.query.calledOnce.should.be.true(); + shared.pipeline.STAGES.serialisation.output.calledOnce.should.be.true(); + + // cache was set + apiController.browse.cache.set.calledOnce.should.be.true(); + apiController.browse.cache.set.args[0][1].should.equal('response'); + }); + + 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) + } + } + }; + + const apiUtils = {}; + const result = shared.pipeline(apiController, apiUtils); + + 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; + }); + + const response = await result.browse(); + + response.should.eql('CACHED RESPONSE'); + + // request went through all stages + shared.pipeline.STAGES.validation.input.calledOnce.should.be.false(); + shared.pipeline.STAGES.serialisation.input.calledOnce.should.be.false(); + shared.pipeline.STAGES.permissions.calledOnce.should.be.false(); + shared.pipeline.STAGES.query.calledOnce.should.be.false(); + shared.pipeline.STAGES.serialisation.output.calledOnce.should.be.false(); + + // cache not set + apiController.browse.cache.set.calledOnce.should.be.false(); + }); + }); }); From b2e2a8025036d5905af3386215bd5005c64195a4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Feb 2023 17:40:33 +0000 Subject: [PATCH 024/118] Update @tryghost --- ghost/api-framework/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 5ff94957bda..a126ab953f9 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -24,11 +24,11 @@ "sinon": "14.0.2" }, "dependencies": { - "@tryghost/debug": "0.1.21", + "@tryghost/debug": "0.1.22", "@tryghost/errors": "1.2.21", - "@tryghost/promise": "0.1.22", - "@tryghost/tpl": "0.1.21", - "@tryghost/validator": "0.1.31", + "@tryghost/promise": "0.3.2", + "@tryghost/tpl": "0.1.22", + "@tryghost/validator": "0.2.1", "jsonpath": "1.1.1", "lodash": "4.17.21" } From 4a1880fa51ad16490e2a8f73375d263c36bd9335 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Thu, 2 Mar 2023 09:57:57 +0100 Subject: [PATCH 025/118] Updated `sinon` dependency - this is being done manually instead of merging the Renovate PR because the PR bundles another bump which doesn't pass yet --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index a126ab953f9..7a4ade5c745 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -21,7 +21,7 @@ "c8": "7.13.0", "mocha": "10.2.0", "should": "13.2.3", - "sinon": "14.0.2" + "sinon": "15.0.1" }, "dependencies": { "@tryghost/debug": "0.1.22", From 11e50b8123318320804f5e49ea8fd5a945b71f95 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 Mar 2023 02:36:20 +0000 Subject: [PATCH 026/118] Update Test & linting packages --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 7a4ade5c745..c568b15e13f 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -21,7 +21,7 @@ "c8": "7.13.0", "mocha": "10.2.0", "should": "13.2.3", - "sinon": "15.0.1" + "sinon": "15.0.2" }, "dependencies": { "@tryghost/debug": "0.1.22", From 661b46e6be6cd19ed06d2fa3784d506d63e1dfbe Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Wed, 5 Apr 2023 09:22:41 +0200 Subject: [PATCH 027/118] Added yarn resolution for `@tryghost/errors` - we keep ending up with multiple versions of the depedency in our tree, and it's causing problems when comparing instances - the workaround I'm implementing for now is to bump the package everywhere and set a resolution so we only have 1 shared instance - hopefully we can come up with a better method down the line --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index c568b15e13f..5c98d9d4f0e 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@tryghost/debug": "0.1.22", - "@tryghost/errors": "1.2.21", + "@tryghost/errors": "1.2.23", "@tryghost/promise": "0.3.2", "@tryghost/tpl": "0.1.22", "@tryghost/validator": "0.2.1", From e276d00a211a4854d7f6d4892c554eb9fd484193 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Wed, 5 Apr 2023 13:22:00 +0200 Subject: [PATCH 028/118] Removed heavy dependency within `@tryghost/errors` - we previously used `@stdlib/utils` instead of the child package `@stdlib/copy`, which is a lot smaller and contains our only use of the parent - this saves 140+MB of dependencies --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 5c98d9d4f0e..829ea6b36ba 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@tryghost/debug": "0.1.22", - "@tryghost/errors": "1.2.23", + "@tryghost/errors": "1.2.24", "@tryghost/promise": "0.3.2", "@tryghost/tpl": "0.1.22", "@tryghost/validator": "0.2.1", From 783b0ced6926d3f7d3aacf569785a24ba8c82d69 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 12:53:35 +0000 Subject: [PATCH 029/118] Update Test & linting packages --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 829ea6b36ba..d562c9fe2c0 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -21,7 +21,7 @@ "c8": "7.13.0", "mocha": "10.2.0", "should": "13.2.3", - "sinon": "15.0.2" + "sinon": "15.0.3" }, "dependencies": { "@tryghost/debug": "0.1.22", From 36afc722a6fec5f7024064bc9e6babf13ada0947 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 7 Apr 2023 11:14:57 +0000 Subject: [PATCH 030/118] Update @tryghost --- ghost/api-framework/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index d562c9fe2c0..f92ad116a1f 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -24,11 +24,11 @@ "sinon": "15.0.3" }, "dependencies": { - "@tryghost/debug": "0.1.22", + "@tryghost/debug": "0.1.24", "@tryghost/errors": "1.2.24", - "@tryghost/promise": "0.3.2", - "@tryghost/tpl": "0.1.22", - "@tryghost/validator": "0.2.1", + "@tryghost/promise": "0.3.4", + "@tryghost/tpl": "0.1.24", + "@tryghost/validator": "0.2.4", "jsonpath": "1.1.1", "lodash": "4.17.21" } From 5767c9cbb79b85e5c3eba5435d3d21bc8a9d2b5c Mon Sep 17 00:00:00 2001 From: "Fabien \"egg\" O'Carroll" Date: Tue, 2 May 2023 16:43:47 -0400 Subject: [PATCH 031/118] Added eslint rule for file naming convention As discussed with the product team we want to enforce kebab-case file names for all files, with the exception of files which export a single class, in which case they should be PascalCase and reflect the class which they export. This will help find classes faster, and should push better naming for them too. Some files and packages have been excluded from this linting, specifically when a library or framework depends on the naming of a file for the functionality e.g. Ember, knex-migrator, adapter-manager --- ghost/api-framework/lib/{frame.js => Frame.js} | 0 ghost/api-framework/lib/api-framework.js | 2 +- ghost/api-framework/lib/http.js | 2 +- ghost/api-framework/lib/pipeline.js | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename ghost/api-framework/lib/{frame.js => Frame.js} (100%) diff --git a/ghost/api-framework/lib/frame.js b/ghost/api-framework/lib/Frame.js similarity index 100% rename from ghost/api-framework/lib/frame.js rename to ghost/api-framework/lib/Frame.js diff --git a/ghost/api-framework/lib/api-framework.js b/ghost/api-framework/lib/api-framework.js index fdda5f7d74c..28fa942163d 100644 --- a/ghost/api-framework/lib/api-framework.js +++ b/ghost/api-framework/lib/api-framework.js @@ -8,7 +8,7 @@ module.exports = { }, get Frame() { - return require('./frame'); + return require('./Frame'); }, get pipeline() { diff --git a/ghost/api-framework/lib/http.js b/ghost/api-framework/lib/http.js index 72848e19a13..3ef7dd0023c 100644 --- a/ghost/api-framework/lib/http.js +++ b/ghost/api-framework/lib/http.js @@ -1,7 +1,7 @@ const url = require('url'); const debug = require('@tryghost/debug')('http'); -const Frame = require('./frame'); +const Frame = require('./Frame'); const headers = require('./headers'); /** diff --git a/ghost/api-framework/lib/pipeline.js b/ghost/api-framework/lib/pipeline.js index 5d6e1bbb0ac..ebbadf384a1 100644 --- a/ghost/api-framework/lib/pipeline.js +++ b/ghost/api-framework/lib/pipeline.js @@ -3,7 +3,7 @@ const _ = require('lodash'); const errors = require('@tryghost/errors'); const {sequence} = require('@tryghost/promise'); -const Frame = require('./frame'); +const Frame = require('./Frame'); const serializers = require('./serializers'); const validators = require('./validators'); From 0330b04c44173c7301edf549ab759407e1a522b7 Mon Sep 17 00:00:00 2001 From: Michael Barrett <991592+mike182uk@users.noreply.github.com> Date: Mon, 15 May 2023 09:30:32 +0100 Subject: [PATCH 032/118] =?UTF-8?q?=E2=9C=A8=20Implemented=20duplicate=20p?= =?UTF-8?q?ost=20functionality=20(#16767)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refs: https://github.com/TryGhost/Team/issues/3139 https://github.com/TryGhost/Team/issues/3140 - Added duplicate post functionality to post list context menu - Currently only a single post can be duplicated at a time - Currently only enabled via the `Making it rain` flag - Added admin API endpoint to copy a post - `POST ghost/api/admin/posts//copy/` - Added admin API endpoint to copy a page - `POST ghost/api/admin/pages//copy/` --- ghost/api-framework/lib/headers.js | 8 ++++- ghost/api-framework/test/headers.test.js | 37 +++++++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/ghost/api-framework/lib/headers.js b/ghost/api-framework/lib/headers.js index 81d9b75d18f..4349777c12b 100644 --- a/ghost/api-framework/lib/headers.js +++ b/ghost/api-framework/lib/headers.js @@ -122,7 +122,8 @@ module.exports = { } const locationHeaderDisabled = apiConfigHeaders?.location === false; - const hasFrameData = frame?.method === 'add' && result[frame.docName]?.[0]?.id; + const hasLocationResolver = apiConfigHeaders?.location?.resolve; + const hasFrameData = (frame?.method === 'add' || hasLocationResolver) && result[frame.docName]?.[0]?.id; if (!locationHeaderDisabled && hasFrameData) { const protocol = (frame.original.url.secure === false) ? 'http://' : 'https://'; @@ -132,8 +133,13 @@ module.exports = { if (!locationURL.endsWith('/')) { locationURL += '/'; } + locationURL += `${resourceId}/`; + if (hasLocationResolver) { + locationURL = apiConfigHeaders.location.resolve(locationURL); + } + const locationHeader = { Location: locationURL }; diff --git a/ghost/api-framework/test/headers.test.js b/ghost/api-framework/test/headers.test.js index 047e92cf7e3..c7ce6dfedd8 100644 --- a/ghost/api-framework/test/headers.test.js +++ b/ghost/api-framework/test/headers.test.js @@ -102,7 +102,7 @@ describe('Headers', function () { }); describe('location header', function () { - it('adds header when all needed data is present', function () { + it('adds header when all needed data is present and method is add', function () { const apiResult = { posts: [{ id: 'id_value' @@ -130,6 +130,41 @@ describe('Headers', function () { }); }); + it('adds header when a location resolver is provided', function () { + const apiResult = { + posts: [{ + id: 'id_value' + }] + }; + + const resolvedLocationUrl = 'resolved location'; + + const apiConfigHeaders = { + location: { + resolve() { + return resolvedLocationUrl; + } + } + }; + const frame = { + docName: 'posts', + method: 'copy', + original: { + url: { + host: 'example.com', + pathname: `/api/content/posts/existing_post_id_value/copy` + } + } + }; + + return shared.headers.get(apiResult, apiConfigHeaders, frame) + .then((result) => { + result.should.eql({ + Location: resolvedLocationUrl + }); + }); + }); + it('respects HTTP redirects', async function () { const apiResult = { posts: [{ From 3a2fcb19eeb601fae513b4ee58f5710a4b54c0dd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Jul 2023 13:11:02 +0000 Subject: [PATCH 033/118] Update Test & linting packages --- ghost/api-framework/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index f92ad116a1f..0304e745957 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -18,10 +18,10 @@ "lib" ], "devDependencies": { - "c8": "7.13.0", + "c8": "7.14.0", "mocha": "10.2.0", "should": "13.2.3", - "sinon": "15.0.3" + "sinon": "15.2.0" }, "dependencies": { "@tryghost/debug": "0.1.24", From 37f7ed8e85a65ffdff87e0871df28a9ae59a34f5 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Wed, 30 Aug 2023 11:33:31 +0200 Subject: [PATCH 034/118] Added function names to all Express middleware refs https://github.com/TryGhost/DevOps/issues/68 - without a name, tools such as New Relic report the function as ``, which makes it incredible hard to follow the code flow - this commit adds a function name to all middleware I can find that doesn't already have one, which should fill in a lot of those gaps --- ghost/api-framework/lib/http.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/lib/http.js b/ghost/api-framework/lib/http.js index 3ef7dd0023c..369ce13aa4f 100644 --- a/ghost/api-framework/lib/http.js +++ b/ghost/api-framework/lib/http.js @@ -14,7 +14,7 @@ const headers = require('./headers'); * @return {Function} */ const http = (apiImpl) => { - return async (req, res, next) => { + return async function Http(req, res, next) { debug(`External API request to ${req.url}`); let apiKey = null; let integration = null; From 12ff4c7ab5b2292ee9232e624a6be4cff9edfba0 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Fri, 1 Sep 2023 15:32:29 +0200 Subject: [PATCH 035/118] Updated linting and testing packages --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 0304e745957..2baaf0910a2 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -18,7 +18,7 @@ "lib" ], "devDependencies": { - "c8": "7.14.0", + "c8": "8.0.1", "mocha": "10.2.0", "should": "13.2.3", "sinon": "15.2.0" From 53882175c77a4df8b8db62c5d3fd33e54f14213f Mon Sep 17 00:00:00 2001 From: "Fabien \"egg\" O'Carroll" Date: Tue, 5 Sep 2023 14:10:20 +0700 Subject: [PATCH 036/118] Added support for custom cache key generation refs https://github.com/TryGhost/Arch/issues/83 This allows endpoints to implement their own key generation, with access to the frame object they can be smart about key generation and use only options and context values that are appropriate. --- ghost/api-framework/lib/pipeline.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ghost/api-framework/lib/pipeline.js b/ghost/api-framework/lib/pipeline.js index ebbadf384a1..7ea64343066 100644 --- a/ghost/api-framework/lib/pipeline.js +++ b/ghost/api-framework/lib/pipeline.js @@ -1,5 +1,6 @@ const debug = require('@tryghost/debug')('pipeline'); const _ = require('lodash'); +const stringify = require('json-stable-stringify'); const errors = require('@tryghost/errors'); const {sequence} = require('@tryghost/promise'); @@ -229,7 +230,13 @@ const pipeline = (apiController, apiUtils, apiType) => { frame.docName = docName; frame.method = method; - let cacheKey = JSON.stringify(frame.options); + let cacheKeyData = frame.options; + if (apiImpl.generateCacheKeyData) { + cacheKeyData = await apiImpl.generateCacheKeyData(frame); + } + + const cacheKey = stringify(cacheKeyData); + if (apiImpl.cache) { const response = await apiImpl.cache.get(cacheKey); From 421a54b02adfda457f80c4aa5963e910239e35f6 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Thu, 5 Oct 2023 12:09:39 +0200 Subject: [PATCH 037/118] Configured all unit tests to use dot reporter refs https://ghost.slack.com/archives/C02G9E68C/p1696490748701419 - this configures mocha to use the dot reporter because the default is way too verbose in CI --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 2baaf0910a2..a635be08c74 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -7,7 +7,7 @@ "main": "index.js", "scripts": { "dev": "echo \"Implement me!\"", - "test:unit": "NODE_ENV=testing c8 --all --reporter text --reporter cobertura mocha './test/**/*.test.js'", + "test:unit": "NODE_ENV=testing c8 --all --reporter text --reporter cobertura -- mocha --reporter dot './test/**/*.test.js'", "test": "yarn test:unit", "lint:code": "eslint *.js lib/ --ext .js --cache", "lint": "yarn lint:code && yarn lint:test", From 4398d0839cab69fccd8bd2ccbf720feec6f633ef Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Thu, 12 Oct 2023 09:50:42 +0200 Subject: [PATCH 038/118] Aligned dependencies with resolution values - this commit brings all dependencies up-to-date with the version set as a resolution --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index a635be08c74..6c0e5ce0963 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@tryghost/debug": "0.1.24", - "@tryghost/errors": "1.2.24", + "@tryghost/errors": "1.2.26", "@tryghost/promise": "0.3.4", "@tryghost/tpl": "0.1.24", "@tryghost/validator": "0.2.4", From 352bbd920eb60b820470dc526cd204cf1ad4e9f4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 31 Oct 2023 19:43:13 +0000 Subject: [PATCH 039/118] Update TryGhost packages --- ghost/api-framework/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 6c0e5ce0963..0310d0ca8ff 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -24,11 +24,11 @@ "sinon": "15.2.0" }, "dependencies": { - "@tryghost/debug": "0.1.24", + "@tryghost/debug": "0.1.26", "@tryghost/errors": "1.2.26", - "@tryghost/promise": "0.3.4", - "@tryghost/tpl": "0.1.24", - "@tryghost/validator": "0.2.4", + "@tryghost/promise": "0.3.6", + "@tryghost/tpl": "0.1.26", + "@tryghost/validator": "0.2.6", "jsonpath": "1.1.1", "lodash": "4.17.21" } From 0e720f06077a015da57b9e423015709c0b14bb6b Mon Sep 17 00:00:00 2001 From: Fabien 'egg' O'Carroll Date: Wed, 17 Jan 2024 20:00:24 +0700 Subject: [PATCH 040/118] Added support for "Refresh Ahead" caching strategy (#19499) The main changes are: - Updating the pipeline to allow for doing a background refresh of the cache - Remove the use of the EventAwareCacheWrapper for the posts public cache ### Background refresh This is just an initial implementation, and tbh it doesn't sit right with me that the logic for this is in the pipeline - I think this should sit in the cache implementation itself, and then we call out to it with something like: `cache.get(key, fetchData)` and then the updates can happen internally. The `cache-manager` project actually has a method like this called `wrap` - but every time I've used it it hangs, and debugging was a pain, so I don't really trust it. ### EventAwareCacheWrapper This is such a small amount of logic, I don't think it's worth creating an entire wrapper for it, at least not a class based one. I would be happy to refactor this to use a `Proxy` too, so that we don't have to add methods to it each time we wanna change the underlying cache implementation. --- ghost/api-framework/lib/pipeline.js | 41 ++++++++++++----------------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/ghost/api-framework/lib/pipeline.js b/ghost/api-framework/lib/pipeline.js index 7ea64343066..c4e164cdbe7 100644 --- a/ghost/api-framework/lib/pipeline.js +++ b/ghost/api-framework/lib/pipeline.js @@ -238,35 +238,28 @@ const pipeline = (apiController, apiUtils, apiType) => { const cacheKey = stringify(cacheKeyData); if (apiImpl.cache) { - const response = await apiImpl.cache.get(cacheKey); - + const response = await apiImpl.cache.get(cacheKey, getResponse); if (response) { return Promise.resolve(response); } } - return Promise.resolve() - .then(() => { - return STAGES.validation.input(apiUtils, apiConfig, apiImpl, frame); - }) - .then(() => { - return STAGES.serialisation.input(apiUtils, apiConfig, apiImpl, frame); - }) - .then(() => { - return STAGES.permissions(apiUtils, apiConfig, apiImpl, frame); - }) - .then(() => { - return STAGES.query(apiUtils, apiConfig, apiImpl, frame); - }) - .then((response) => { - return STAGES.serialisation.output(response, apiUtils, apiConfig, apiImpl, frame); - }) - .then(async () => { - if (apiImpl.cache) { - await apiImpl.cache.set(cacheKey, frame.response); - } - return frame.response; - }); + async function getResponse() { + await STAGES.validation.input(apiUtils, apiConfig, apiImpl, frame); + await STAGES.serialisation.input(apiUtils, apiConfig, apiImpl, frame); + await STAGES.permissions(apiUtils, apiConfig, apiImpl, frame); + const response = await STAGES.query(apiUtils, apiConfig, apiImpl, frame); + await STAGES.serialisation.output(response, apiUtils, apiConfig, apiImpl, frame); + return frame.response; + } + + const response = await getResponse(); + + if (apiImpl.cache) { + await apiImpl.cache.set(cacheKey, response); + } + + return response; }; Object.assign(obj[method], apiImpl); From 62fed6d9af524f0740c39fd466f1ec43b67a5273 Mon Sep 17 00:00:00 2001 From: Simon Backx Date: Wed, 17 Jan 2024 14:12:58 +0100 Subject: [PATCH 041/118] Revert "Added support for "Refresh Ahead" caching strategy" (#19502) Reverts TryGhost/Ghost#19499 --- ghost/api-framework/lib/pipeline.js | 41 +++++++++++++++++------------ 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/ghost/api-framework/lib/pipeline.js b/ghost/api-framework/lib/pipeline.js index c4e164cdbe7..7ea64343066 100644 --- a/ghost/api-framework/lib/pipeline.js +++ b/ghost/api-framework/lib/pipeline.js @@ -238,28 +238,35 @@ const pipeline = (apiController, apiUtils, apiType) => { const cacheKey = stringify(cacheKeyData); if (apiImpl.cache) { - const response = await apiImpl.cache.get(cacheKey, getResponse); + const response = await apiImpl.cache.get(cacheKey); + if (response) { return Promise.resolve(response); } } - async function getResponse() { - await STAGES.validation.input(apiUtils, apiConfig, apiImpl, frame); - await STAGES.serialisation.input(apiUtils, apiConfig, apiImpl, frame); - await STAGES.permissions(apiUtils, apiConfig, apiImpl, frame); - const response = await STAGES.query(apiUtils, apiConfig, apiImpl, frame); - await STAGES.serialisation.output(response, apiUtils, apiConfig, apiImpl, frame); - return frame.response; - } - - const response = await getResponse(); - - if (apiImpl.cache) { - await apiImpl.cache.set(cacheKey, response); - } - - return response; + return Promise.resolve() + .then(() => { + return STAGES.validation.input(apiUtils, apiConfig, apiImpl, frame); + }) + .then(() => { + return STAGES.serialisation.input(apiUtils, apiConfig, apiImpl, frame); + }) + .then(() => { + return STAGES.permissions(apiUtils, apiConfig, apiImpl, frame); + }) + .then(() => { + return STAGES.query(apiUtils, apiConfig, apiImpl, frame); + }) + .then((response) => { + return STAGES.serialisation.output(response, apiUtils, apiConfig, apiImpl, frame); + }) + .then(async () => { + if (apiImpl.cache) { + await apiImpl.cache.set(cacheKey, frame.response); + } + return frame.response; + }); }; Object.assign(obj[method], apiImpl); From b6509196007c7e1191ada943d933b5029e92ebfe Mon Sep 17 00:00:00 2001 From: "Fabien \"egg\" O'Carroll" Date: Wed, 17 Jan 2024 11:28:45 +0700 Subject: [PATCH 042/118] Refactored the pipeline execution to async fn Having the code use `async/await` make it more readable, and extracting the execution to a separate function make its easier to run in the background in the future --- ghost/api-framework/lib/pipeline.js | 38 ++++++++++++----------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/ghost/api-framework/lib/pipeline.js b/ghost/api-framework/lib/pipeline.js index 7ea64343066..1258a88034f 100644 --- a/ghost/api-framework/lib/pipeline.js +++ b/ghost/api-framework/lib/pipeline.js @@ -245,28 +245,22 @@ const pipeline = (apiController, apiUtils, apiType) => { } } - return Promise.resolve() - .then(() => { - return STAGES.validation.input(apiUtils, apiConfig, apiImpl, frame); - }) - .then(() => { - return STAGES.serialisation.input(apiUtils, apiConfig, apiImpl, frame); - }) - .then(() => { - return STAGES.permissions(apiUtils, apiConfig, apiImpl, frame); - }) - .then(() => { - return STAGES.query(apiUtils, apiConfig, apiImpl, frame); - }) - .then((response) => { - return STAGES.serialisation.output(response, apiUtils, apiConfig, apiImpl, frame); - }) - .then(async () => { - if (apiImpl.cache) { - await apiImpl.cache.set(cacheKey, frame.response); - } - return frame.response; - }); + async function getResponse() { + await STAGES.validation.input(apiUtils, apiConfig, apiImpl, frame); + await STAGES.serialisation.input(apiUtils, apiConfig, apiImpl, frame); + await STAGES.permissions(apiUtils, apiConfig, apiImpl, frame); + const response = await STAGES.query(apiUtils, apiConfig, apiImpl, frame); + await STAGES.serialisation.output(response, apiUtils, apiConfig, apiImpl, frame); + return frame.response; + } + + const response = await getResponse(); + + if (apiImpl.cache) { + await apiImpl.cache.set(cacheKey, response); + } + + return response; }; Object.assign(obj[method], apiImpl); From 685d4a10d06e0f2b83d98051a60c79c1fc9a0e22 Mon Sep 17 00:00:00 2001 From: "Fabien \"egg\" O'Carroll" Date: Wed, 17 Jan 2024 13:32:26 +0700 Subject: [PATCH 043/118] Implemented Refresh-Ahead caching for Redis This updates the AdapterCacheRedis instance to be able to handle updating itself when reading from the cache. For this to work we need to pass a `fetchData` function to the `get` method. In the case of a cache miss, we will read the data via the `fetchData` function, and store it in the cache, before returning the value to the caller. When coupled with a `refreshAheadFactor` config, we will go a step further and implement the "Refresh Ahead" caching strategy. What this means is that we will refresh the contents of the cache in the background, this happens on a cache read and only once the data in the cache has only a certain percentage of the TTL left, which is set as a decimal value between 0 and 1. e.g. ttl = 100s refreshAheadFactor = 0.2; Any read from the cache that happens _after_ 80s will do a background refresh --- ghost/api-framework/lib/pipeline.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ghost/api-framework/lib/pipeline.js b/ghost/api-framework/lib/pipeline.js index 1258a88034f..c4e164cdbe7 100644 --- a/ghost/api-framework/lib/pipeline.js +++ b/ghost/api-framework/lib/pipeline.js @@ -238,8 +238,7 @@ const pipeline = (apiController, apiUtils, apiType) => { const cacheKey = stringify(cacheKeyData); if (apiImpl.cache) { - const response = await apiImpl.cache.get(cacheKey); - + const response = await apiImpl.cache.get(cacheKey, getResponse); if (response) { return Promise.resolve(response); } From e9dffeeef7241d0cc39dc5becde904139a4919c9 Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Tue, 27 Feb 2024 16:13:08 -0500 Subject: [PATCH 044/118] Supported setting headers on a per-request basis refs https://linear.app/tryghost/issue/ENG-674 This paves the way for us to have dynamic cache invalidation headers without clobbering the shared headers config. --- ghost/api-framework/lib/Frame.js | 10 +++ ghost/api-framework/lib/headers.js | 4 + ghost/api-framework/test/headers.test.js | 97 ++++++++++++------------ 3 files changed, 61 insertions(+), 50 deletions(-) diff --git a/ghost/api-framework/lib/Frame.js b/ghost/api-framework/lib/Frame.js index 2be8cb3b747..70d265bb8a1 100644 --- a/ghost/api-framework/lib/Frame.js +++ b/ghost/api-framework/lib/Frame.js @@ -9,6 +9,7 @@ const _ = require('lodash'); * easiest to use. We always have access to the original input, we never loose track of it. */ class Frame { + #headers = {}; constructor(obj = {}) { this.original = obj; @@ -27,6 +28,7 @@ class Frame { this.user = {}; this.file = {}; this.files = []; + this.#headers = {}; this.apiType = null; this.docName = null; this.method = null; @@ -95,6 +97,14 @@ class Frame { debug('options', this.options); debug('data', this.data); } + + setHeader(header, value) { + this.#headers[header] = value; + } + + getHeaders() { + return Object.assign({}, this.#headers); + } } module.exports = Frame; diff --git a/ghost/api-framework/lib/headers.js b/ghost/api-framework/lib/headers.js index 4349777c12b..f235e82906d 100644 --- a/ghost/api-framework/lib/headers.js +++ b/ghost/api-framework/lib/headers.js @@ -147,6 +147,10 @@ module.exports = { Object.assign(headers, locationHeader); } + const headersFromFrame = frame.getHeaders(); + + Object.assign(headers, headersFromFrame); + debug(headers); return headers; } diff --git a/ghost/api-framework/test/headers.test.js b/ghost/api-framework/test/headers.test.js index c7ce6dfedd8..941ba9160b3 100644 --- a/ghost/api-framework/test/headers.test.js +++ b/ghost/api-framework/test/headers.test.js @@ -1,15 +1,16 @@ const shared = require('../'); +const Frame = require('../lib/Frame'); describe('Headers', function () { it('empty headers config', function () { - return shared.headers.get().then((result) => { + return shared.headers.get({}, {}, new Frame()).then((result) => { result.should.eql({}); }); }); describe('config.disposition', function () { it('json', function () { - return shared.headers.get({}, {disposition: {type: 'json', value: 'value'}}) + return shared.headers.get({}, {disposition: {type: 'json', value: 'value'}}, new Frame()) .then((result) => { result.should.eql({ 'Content-Disposition': 'Attachment; filename="value"', @@ -20,7 +21,7 @@ describe('Headers', function () { }); it('csv', function () { - return shared.headers.get({}, {disposition: {type: 'csv', value: 'my.csv'}}) + return shared.headers.get({}, {disposition: {type: 'csv', value: 'my.csv'}}, new Frame()) .then((result) => { result.should.eql({ 'Content-Disposition': 'Attachment; filename="my.csv"', @@ -39,7 +40,7 @@ describe('Headers', function () { return filename; } } - }); + }, new Frame()); result.should.eql({ 'Content-Disposition': 'Attachment; filename="awesome-data-2022-08-01.csv"', 'Content-Type': 'text/csv' @@ -47,7 +48,7 @@ describe('Headers', function () { }); it('file', async function () { - const result = await shared.headers.get({}, {disposition: {type: 'file', value: 'my.txt'}}); + const result = await shared.headers.get({}, {disposition: {type: 'file', value: 'my.txt'}}, new Frame()); result.should.eql({ 'Content-Disposition': 'Attachment; filename="my.txt"' }); @@ -63,14 +64,14 @@ describe('Headers', function () { return filename; } } - }); + }, new Frame()); result.should.eql({ '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'}}) + return shared.headers.get('yaml file', {disposition: {type: 'yaml', value: 'my.yaml'}}, new Frame()) .then((result) => { result.should.eql({ 'Content-Disposition': 'Attachment; filename="my.yaml"', @@ -83,7 +84,7 @@ describe('Headers', function () { describe('config.cacheInvalidate', function () { it('default', function () { - return shared.headers.get({}, {cacheInvalidate: true}) + return shared.headers.get({}, {cacheInvalidate: true}, new Frame()) .then((result) => { result.should.eql({ 'X-Cache-Invalidate': '/*' @@ -92,7 +93,7 @@ describe('Headers', function () { }); it('custom value', function () { - return shared.headers.get({}, {cacheInvalidate: {value: 'value'}}) + return shared.headers.get({}, {cacheInvalidate: {value: 'value'}}, new Frame()) .then((result) => { result.should.eql({ 'X-Cache-Invalidate': 'value' @@ -110,14 +111,13 @@ describe('Headers', function () { }; const apiConfigHeaders = {}; - const frame = { - docName: 'posts', - method: 'add', - original: { - url: { - host: 'example.com', - pathname: `/api/content/posts/` - } + const frame = new Frame(); + frame.docName = 'posts', + frame.method = 'add', + frame.original = { + url: { + host: 'example.com', + pathname: `/api/content/posts/` } }; @@ -146,14 +146,13 @@ describe('Headers', function () { } } }; - const frame = { - docName: 'posts', - method: 'copy', - original: { - url: { - host: 'example.com', - pathname: `/api/content/posts/existing_post_id_value/copy` - } + const frame = new Frame(); + frame.docName = 'posts'; + frame.method = 'copy'; + frame.original = { + url: { + host: 'example.com', + pathname: `/api/content/posts/existing_post_id_value/copy` } }; @@ -173,15 +172,15 @@ describe('Headers', function () { }; const apiConfigHeaders = {}; - const frame = { - docName: 'posts', - method: 'add', - original: { - url: { - host: 'example.com', - pathname: `/api/content/posts/`, - secure: false - } + const frame = new Frame(); + + frame.docName = 'posts'; + frame.method = 'add'; + frame.original = { + url: { + host: 'example.com', + pathname: `/api/content/posts/`, + secure: false } }; @@ -200,14 +199,13 @@ describe('Headers', function () { }; const apiConfigHeaders = {}; - const frame = { - docName: 'posts', - method: 'add', - original: { - url: { - host: 'example.com', - pathname: `/api/content/posts` - } + const frame = new Frame(); + frame.docName = 'posts'; + frame.method = 'add'; + frame.original = { + url: { + host: 'example.com', + pathname: `/api/content/posts` } }; @@ -224,14 +222,13 @@ describe('Headers', function () { const apiResult = {}; const apiConfigHeaders = {}; - const frame = { - docName: 'posts', - method: 'add', - original: { - url: { - host: 'example.com', - pathname: `/api/content/posts/` - } + const frame = new Frame(); + frame.docName = 'posts'; + frame.method = 'add'; + frame.original = { + url: { + host: 'example.com', + pathname: `/api/content/posts/` } }; From b74e547e799e6b0fb97fabdf3e1b254a084706d4 Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Tue, 27 Feb 2024 16:17:06 -0500 Subject: [PATCH 045/118] Ensured that endpoint header config is not modified in future refs https://linear.app/tryghost/issue/ENG-674/ This will cause errors to be thrown if developers attempt to modify the shared header config in future. --- ghost/api-framework/lib/pipeline.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ghost/api-framework/lib/pipeline.js b/ghost/api-framework/lib/pipeline.js index c4e164cdbe7..8d4a4b1e152 100644 --- a/ghost/api-framework/lib/pipeline.js +++ b/ghost/api-framework/lib/pipeline.js @@ -188,6 +188,8 @@ const pipeline = (apiController, apiUtils, apiType) => { return keys.reduce((obj, method) => { const apiImpl = _.cloneDeep(apiController)[method]; + Object.freeze(apiImpl.headers); + obj[method] = async function wrapper() { const apiConfig = {docName, method}; let options; From fb73dbcb259a7de3c85b5a678bff5217e8a7d548 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Mon, 11 Mar 2024 17:14:14 +0100 Subject: [PATCH 046/118] Updated `@tryghost/errors` dependency - this version is written in TS, but was published a few months ago and needs to be bumped here - also updates a previous deep include into the library, which was unnecessary anyway --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 0310d0ca8ff..ee4a6ab3fe8 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@tryghost/debug": "0.1.26", - "@tryghost/errors": "1.2.26", + "@tryghost/errors": "1.3.1", "@tryghost/promise": "0.3.6", "@tryghost/tpl": "0.1.26", "@tryghost/validator": "0.2.6", From 36cfa9c0578b6b804c791e3c4cf25f6fa720de59 Mon Sep 17 00:00:00 2001 From: Fabien 'egg' O'Carroll Date: Tue, 19 Mar 2024 00:29:41 +0700 Subject: [PATCH 047/118] Cached api controller pipelines (#19880) ref ENG-761 ref https://linear.app/tryghost/issue/ENG-761 Creating these pipelines is expensive, and we don't want to do it repeatedly for the same controller. Adding caching should reduce the amount of time spent setting up pipelines for each usage of the `get` helper. --- ghost/api-framework/lib/pipeline.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ghost/api-framework/lib/pipeline.js b/ghost/api-framework/lib/pipeline.js index 8d4a4b1e152..bf60b5d03be 100644 --- a/ghost/api-framework/lib/pipeline.js +++ b/ghost/api-framework/lib/pipeline.js @@ -160,6 +160,8 @@ const STAGES = { } }; +const controllerMap = new Map(); + /** * @description The pipeline runs the request through all stages (validation, serialisation, permissions). * @@ -180,12 +182,16 @@ const STAGES = { * @return {Object} */ const pipeline = (apiController, apiUtils, apiType) => { + if (controllerMap.has(apiController)) { + return controllerMap.get(apiController); + } + const keys = Object.keys(apiController); const docName = apiController.docName; // CASE: api controllers are objects with configuration. // We have to ensure that we expose a functional interface e.g. `api.posts.add` has to be available. - return keys.reduce((obj, method) => { + const result = keys.reduce((obj, method) => { const apiImpl = _.cloneDeep(apiController)[method]; Object.freeze(apiImpl.headers); @@ -267,6 +273,10 @@ const pipeline = (apiController, apiUtils, apiType) => { Object.assign(obj[method], apiImpl); return obj; }, {}); + + controllerMap.set(apiController, result); + + return result; }; module.exports = pipeline; From 77274db0ce5ae7c2e172be0297b496f1a61176e6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2024 20:04:58 +0000 Subject: [PATCH 048/118] Update TryGhost packages --- ghost/api-framework/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index ee4a6ab3fe8..0c3d3ffc73f 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -24,11 +24,11 @@ "sinon": "15.2.0" }, "dependencies": { - "@tryghost/debug": "0.1.26", + "@tryghost/debug": "0.1.28", "@tryghost/errors": "1.3.1", - "@tryghost/promise": "0.3.6", - "@tryghost/tpl": "0.1.26", - "@tryghost/validator": "0.2.6", + "@tryghost/promise": "0.3.8", + "@tryghost/tpl": "0.1.28", + "@tryghost/validator": "0.2.9", "jsonpath": "1.1.1", "lodash": "4.17.21" } From 23fa94a90b3458a4e057f63c35e3d8bdc3abee7e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 14:51:33 +0000 Subject: [PATCH 049/118] Update TryGhost packages --- ghost/api-framework/package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 0c3d3ffc73f..71e02c6bcf8 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -24,11 +24,11 @@ "sinon": "15.2.0" }, "dependencies": { - "@tryghost/debug": "0.1.28", - "@tryghost/errors": "1.3.1", - "@tryghost/promise": "0.3.8", - "@tryghost/tpl": "0.1.28", - "@tryghost/validator": "0.2.9", + "@tryghost/debug": "0.1.29", + "@tryghost/errors": "1.3.2", + "@tryghost/promise": "0.3.9", + "@tryghost/tpl": "0.1.29", + "@tryghost/validator": "0.2.10", "jsonpath": "1.1.1", "lodash": "4.17.21" } From 8e550a423e6d8dc62ebd3925dfe6ce338979cdb0 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Mon, 6 May 2024 21:16:43 +0200 Subject: [PATCH 050/118] Fixed miscellaneous jsdoc comments - this helps tsserver figure out what the type of things is around our codebase - nothing crazy, mostly Express types for the middleware, application and router levels --- ghost/api-framework/lib/http.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ghost/api-framework/lib/http.js b/ghost/api-framework/lib/http.js index 369ce13aa4f..65188185f83 100644 --- a/ghost/api-framework/lib/http.js +++ b/ghost/api-framework/lib/http.js @@ -11,9 +11,14 @@ const headers = require('./headers'); * The wrapper receives the express request, prepares the frame and forwards the request to the pipeline. * * @param {Function} apiImpl - Pipeline wrapper, which executes the target ctrl function. - * @return {Function} + * @return {import('express').RequestHandler} */ const http = (apiImpl) => { + /** + * @param {import('express').Request} req - Express request object. + * @param {import('express').Response} res - Express response object. + * @param {import('express').NextFunction} next - Express next function. + */ return async function Http(req, res, next) { debug(`External API request to ${req.url}`); let apiKey = null; From db2d51bf04c10c6c793d936357335eb2bcb1a7cb Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Tue, 7 May 2024 08:36:44 +0200 Subject: [PATCH 051/118] Added JSDoc types for API controllers - this adds a simple set of types to the @tryghost/api-framework package that should describe all of the keys available on a controller, and then rolls it out to all API controllers - unfortunately, due to https://github.com/microsoft/TypeScript/issues/47107, we have to split apart `module.exports` into a variable assignment in order for type-checking to be done - the main benefit of this is that `frame` is now typed, and editors understand what keys are available, so intellisense works properly --- ghost/api-framework/index.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/ghost/api-framework/index.js b/ghost/api-framework/index.js index 373c844ba24..2fd7fa32f49 100644 --- a/ghost/api-framework/index.js +++ b/ghost/api-framework/index.js @@ -1 +1,27 @@ +/** @typedef {object} PermissionsObject */ +/** @typedef {boolean} PermissionsBoolean */ + +/** @typedef {number} StatusCodeNumber */ +/** @typedef {(result: any) => number} StatusCodeFunction */ + +/** @typedef {object} ValidationObject */ + +/** + * @typedef {object} ControllerMethod + * @property {object} headers + * @property {PermissionsBoolean | PermissionsObject} permissions + * @property {string[]} [options] + * @property {ValidationObject} [validation] + * @property {string[]} [data] + * @property {StatusCodeFunction | StatusCodeNumber} [statusCode] + * @property {object} [response] + * @property {function} [cache] + * @property {(frame: import('./lib/Frame')) => object} [generateCacheKeyData] + * @property {(frame: import('./lib/Frame')) => any} query + */ + +/** + * @typedef {Record & Record<'docName', string>} Controller + */ + module.exports = require('./lib/api-framework'); From d8b1ee470b03b4431480824c38177e0adcdb38d8 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Wed, 8 May 2024 09:17:41 +0200 Subject: [PATCH 052/118] Rolled out API framework JSDoc typing to more places - this updates a bunch of places where we're just using Object to cheat the system - doing this means editor autocomplete and basic type checking is better because we now have proper types in place - functionality should not change, these are just comments --- ghost/api-framework/lib/headers.js | 4 ++-- ghost/api-framework/lib/http.js | 3 ++- ghost/api-framework/lib/pipeline.js | 22 +++++++++---------- ghost/api-framework/lib/serializers/handle.js | 4 ++-- ghost/api-framework/lib/validators/handle.js | 2 +- .../api-framework/lib/validators/input/all.js | 16 ++++++++++++++ 6 files changed, 34 insertions(+), 17 deletions(-) diff --git a/ghost/api-framework/lib/headers.js b/ghost/api-framework/lib/headers.js index f235e82906d..165baeb377a 100644 --- a/ghost/api-framework/lib/headers.js +++ b/ghost/api-framework/lib/headers.js @@ -99,8 +99,8 @@ module.exports = { * * @param {Object} result - API response * @param {Object} apiConfigHeaders - * @param {Object} frame - * @return {Promise} + * @param {import('@tryghost/api-framework').Frame} frame + * @return {Promise} */ async get(result, apiConfigHeaders = {}, frame) { let headers = {}; diff --git a/ghost/api-framework/lib/http.js b/ghost/api-framework/lib/http.js index 65188185f83..cf35d705591 100644 --- a/ghost/api-framework/lib/http.js +++ b/ghost/api-framework/lib/http.js @@ -10,7 +10,7 @@ const headers = require('./headers'); * This wrapper is used in the routes definition (see web/). * The wrapper receives the express request, prepares the frame and forwards the request to the pipeline. * - * @param {Function} apiImpl - Pipeline wrapper, which executes the target ctrl function. + * @param {import('@tryghost/api-framework').Controller} apiImpl - Pipeline wrapper, which executes the target ctrl function. * @return {import('express').RequestHandler} */ const http = (apiImpl) => { @@ -18,6 +18,7 @@ const http = (apiImpl) => { * @param {import('express').Request} req - Express request object. * @param {import('express').Response} res - Express response object. * @param {import('express').NextFunction} next - Express next function. + * @returns {Promise} */ return async function Http(req, res, next) { debug(`External API request to ${req.url}`); diff --git a/ghost/api-framework/lib/pipeline.js b/ghost/api-framework/lib/pipeline.js index bf60b5d03be..a2dfa484401 100644 --- a/ghost/api-framework/lib/pipeline.js +++ b/ghost/api-framework/lib/pipeline.js @@ -20,8 +20,8 @@ const STAGES = { * * @param {Object} apiUtils - Local utils of target API version. * @param {Object} apiConfig - Docname & Method of ctrl. - * @param {Object} apiImpl - Controller configuration. - * @param {Object} frame + * @param {import('@tryghost/api-framework').ControllerMethod} apiImpl - Controller configuration. + * @param {import('@tryghost/api-framework').Frame} frame * @return {Promise} */ input(apiUtils, apiConfig, apiImpl, frame) { @@ -57,8 +57,8 @@ const STAGES = { * * @param {Object} apiUtils - Local utils of target API version. * @param {Object} apiConfig - Docname & Method of ctrl. - * @param {Object} apiImpl - Controller configuration. - * @param {Object} frame + * @param {import('@tryghost/api-framework').ControllerMethod} apiImpl - Controller configuration. + * @param {import('@tryghost/api-framework').Frame} frame * @return {Promise} */ input(apiUtils, apiConfig, apiImpl, frame) { @@ -80,8 +80,8 @@ const STAGES = { * * @param {Object} apiUtils - Local utils of target API version. * @param {Object} apiConfig - Docname & Method of ctrl. - * @param {Object} apiImpl - Controller configuration. - * @param {Object} frame + * @param {import('@tryghost/api-framework').ControllerMethod} apiImpl - Controller configuration. + * @param {import('@tryghost/api-framework').Frame} frame * @return {Promise} */ output(response, apiUtils, apiConfig, apiImpl, frame) { @@ -99,8 +99,8 @@ const STAGES = { * * @param {Object} apiUtils - Local utils of target API version. * @param {Object} apiConfig - Docname & Method of ctrl. - * @param {Object} apiImpl - Controller configuration. - * @param {Object} frame + * @param {import('@tryghost/api-framework').ControllerMethod} apiImpl - Controller configuration. + * @param {import('@tryghost/api-framework').Frame} frame * @return {Promise} */ permissions(apiUtils, apiConfig, apiImpl, frame) { @@ -145,8 +145,8 @@ const STAGES = { * * @param {Object} apiUtils - Local utils of target API version. * @param {Object} apiConfig - Docname & Method of ctrl. - * @param {Object} apiImpl - Controller configuration. - * @param {Object} frame + * @param {import('@tryghost/api-framework').ControllerMethod} apiImpl - Controller configuration. + * @param {import('@tryghost/api-framework').Frame} frame * @return {Promise} */ query(apiUtils, apiConfig, apiImpl, frame) { @@ -176,7 +176,7 @@ const controllerMap = new Map(); * 4. Controller - Execute the controller implementation & receive model response. * 5. Output Serialisation - Output formatting, Deprecations, Extra attributes etc... * - * @param {Object} apiController + * @param {import('@tryghost/api-framework').Controller} apiController * @param {Object} apiUtils - Local utils (validation & serialisation) from target API version * @param {String} [apiType] - Content or Admin API access * @return {Object} diff --git a/ghost/api-framework/lib/serializers/handle.js b/ghost/api-framework/lib/serializers/handle.js index c86fe1bdfaa..6356ebdbf35 100644 --- a/ghost/api-framework/lib/serializers/handle.js +++ b/ghost/api-framework/lib/serializers/handle.js @@ -12,7 +12,7 @@ const errors = require('@tryghost/errors'); * * @param {Object} apiConfig - Docname + method of the ctrl * @param {Object} apiSerializers - Target API serializers - * @param {Object} frame + * @param {import('@tryghost/api-framework').Frame} frame */ module.exports.input = (apiConfig, apiSerializers, frame) => { debug('input'); @@ -90,7 +90,7 @@ const getBestMatchSerializer = function (apiSerializers, docName, method) { * @param {Object} response - API response * @param {Object} apiConfig - Docname + method of the ctrl * @param {Object} apiSerializers - Target API serializers - * @param {Object} frame + * @param {import('@tryghost/api-framework').Frame} frame */ module.exports.output = (response = {}, apiConfig, apiSerializers, frame) => { debug('output'); diff --git a/ghost/api-framework/lib/validators/handle.js b/ghost/api-framework/lib/validators/handle.js index ab9901ea9f0..43f62f77ddc 100644 --- a/ghost/api-framework/lib/validators/handle.js +++ b/ghost/api-framework/lib/validators/handle.js @@ -12,7 +12,7 @@ const {sequence} = require('@tryghost/promise'); * * @param {Object} apiConfig - Docname + method of the ctrl * @param {Object} apiValidators - Target API validators - * @param {Object} frame + * @param {import('@tryghost/api-framework').Frame} frame */ module.exports.input = (apiConfig, apiValidators, frame) => { debug('input begin'); diff --git a/ghost/api-framework/lib/validators/input/all.js b/ghost/api-framework/lib/validators/input/all.js index f6e4efc664b..fadb17bb2c8 100644 --- a/ghost/api-framework/lib/validators/input/all.js +++ b/ghost/api-framework/lib/validators/input/all.js @@ -90,6 +90,10 @@ const validate = (config, attrs) => { }; module.exports = { + /** + * @param {object} apiConfig + * @param {import('@tryghost/api-framework').Frame} frame + */ all(apiConfig, frame) { debug('validate all'); @@ -102,6 +106,10 @@ module.exports = { return Promise.resolve(); }, + /** + * @param {object} apiConfig + * @param {import('@tryghost/api-framework').Frame} frame + */ browse(apiConfig, frame) { debug('validate browse'); @@ -121,6 +129,10 @@ module.exports = { return this.browse(...arguments); }, + /** + * @param {object} apiConfig + * @param {import('@tryghost/api-framework').Frame} frame + */ add(apiConfig, frame) { debug('validate add'); @@ -168,6 +180,10 @@ module.exports = { } }, + /** + * @param {object} apiConfig + * @param {import('@tryghost/api-framework').Frame} frame + */ edit(apiConfig, frame) { debug('validate edit'); const result = this.add(...arguments); From 42a1669fb30760cb64113bb02f721d723f9214e6 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Wed, 8 May 2024 10:39:18 +0200 Subject: [PATCH 053/118] Excluded `docName` key from API controller method map - due to the structure of our API controllers, the docName and methods are under the same structure - this code loops over the keys of the controller and forms the method map - however, it currently also loops over every character of the docName, so the resulting map contains a weird structure of chars - we don't need the docName for this, so we can just exclude it from the keys - this doesn't change any functionality --- ghost/api-framework/lib/pipeline.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/lib/pipeline.js b/ghost/api-framework/lib/pipeline.js index a2dfa484401..79dc5b424a5 100644 --- a/ghost/api-framework/lib/pipeline.js +++ b/ghost/api-framework/lib/pipeline.js @@ -186,7 +186,7 @@ const pipeline = (apiController, apiUtils, apiType) => { return controllerMap.get(apiController); } - const keys = Object.keys(apiController); + const keys = Object.keys(apiController).filter(key => key !== 'docName'); const docName = apiController.docName; // CASE: api controllers are objects with configuration. From d5a809a75483409c4c0a0d7878b3c0ef9a30f5f7 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Mon, 13 May 2024 14:44:12 +0200 Subject: [PATCH 054/118] Renamed `wrapper` to `ImplWrapper` - helps with debugging and understanding the code flow --- ghost/api-framework/lib/pipeline.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/lib/pipeline.js b/ghost/api-framework/lib/pipeline.js index 79dc5b424a5..436de80465e 100644 --- a/ghost/api-framework/lib/pipeline.js +++ b/ghost/api-framework/lib/pipeline.js @@ -196,7 +196,7 @@ const pipeline = (apiController, apiUtils, apiType) => { Object.freeze(apiImpl.headers); - obj[method] = async function wrapper() { + obj[method] = async function ImplWrapper() { const apiConfig = {docName, method}; let options; let data; From 69ee84cb52bf36e5e91d9db28eb04edd9b90c1ab Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 13:18:12 +0000 Subject: [PATCH 055/118] Update TryGhost packages --- ghost/api-framework/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 71e02c6bcf8..82afad8e199 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -24,11 +24,11 @@ "sinon": "15.2.0" }, "dependencies": { - "@tryghost/debug": "0.1.29", + "@tryghost/debug": "0.1.30", "@tryghost/errors": "1.3.2", - "@tryghost/promise": "0.3.9", - "@tryghost/tpl": "0.1.29", - "@tryghost/validator": "0.2.10", + "@tryghost/promise": "0.3.10", + "@tryghost/tpl": "0.1.30", + "@tryghost/validator": "0.2.11", "jsonpath": "1.1.1", "lodash": "4.17.21" } From b208f5da7d9b1a142cfbcf523fc2d8c94eeda550 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Mon, 24 Jun 2024 10:01:36 +0200 Subject: [PATCH 056/118] Fixed handling objects as API input parameters fix https://linear.app/tryghost/issue/SLO-155/paramsmap-is-not-a-function-an-unexpected-error-occurred-please-try - in the case you provide an object to the API, this code will throw an error because it can't map over an object - we can just assert that params should be an array and throw an error otherwise --- ghost/api-framework/lib/utils/options.js | 9 +++++++++ ghost/api-framework/test/util/options.test.js | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/ghost/api-framework/lib/utils/options.js b/ghost/api-framework/lib/utils/options.js index 22dd5759258..90b5d6eaa45 100644 --- a/ghost/api-framework/lib/utils/options.js +++ b/ghost/api-framework/lib/utils/options.js @@ -1,4 +1,5 @@ const _ = require('lodash'); +const {IncorrectUsageError} = require('@tryghost/errors'); /** * @description Helper function to prepare params for internal usages. @@ -15,6 +16,14 @@ const trimAndLowerCase = (params) => { 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' + }); + } + return params.map((item) => { return item.trim().toLowerCase(); }); diff --git a/ghost/api-framework/test/util/options.test.js b/ghost/api-framework/test/util/options.test.js index d9812a41cee..ad926f8991c 100644 --- a/ghost/api-framework/test/util/options.test.js +++ b/ghost/api-framework/test/util/options.test.js @@ -20,4 +20,12 @@ describe('util/options', function () { it('accepts parameters in form of an array', function () { optionsUtil.trimAndLowerCase([' PeanUt', ' buTTer ']).should.eql(['peanut', 'butter']); }); + + it('throws error for invalid object input', function () { + try { + optionsUtil.trimAndLowerCase({name: 'peanut'}); + } catch (err) { + err.message.should.eql('Params must be a string or array'); + } + }); }); From ffc1a7155ca6e1984110d601f301fff8333dde99 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Jul 2024 14:14:06 +0000 Subject: [PATCH 057/118] Update TryGhost packages --- ghost/api-framework/package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 82afad8e199..cd4310d1a67 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -24,11 +24,11 @@ "sinon": "15.2.0" }, "dependencies": { - "@tryghost/debug": "0.1.30", - "@tryghost/errors": "1.3.2", - "@tryghost/promise": "0.3.10", - "@tryghost/tpl": "0.1.30", - "@tryghost/validator": "0.2.11", + "@tryghost/debug": "0.1.32", + "@tryghost/errors": "1.3.5", + "@tryghost/promise": "0.3.12", + "@tryghost/tpl": "0.1.32", + "@tryghost/validator": "0.2.14", "jsonpath": "1.1.1", "lodash": "4.17.21" } From 4d4d2ffd5ee849e43a9bae6b949a9a414d945d17 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 29 Mar 2025 21:44:24 +0000 Subject: [PATCH 058/118] Update dependency mocha to v10.8.2 (#22704) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [mocha](https://mochajs.org/) ([source](https://redirect.github.com/mochajs/mocha)) | [`10.2.0` -> `10.8.2`](https://renovatebot.com/diffs/npm/mocha/10.2.0/10.8.2) | [![age](https://developer.mend.io/api/mc/badges/age/npm/mocha/10.8.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/mocha/10.8.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/mocha/10.2.0/10.8.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/mocha/10.2.0/10.8.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [mocha](https://mochajs.org/) ([source](https://redirect.github.com/mochajs/mocha)) | [`10.7.3` -> `10.8.2`](https://renovatebot.com/diffs/npm/mocha/10.7.3/10.8.2) | [![age](https://developer.mend.io/api/mc/badges/age/npm/mocha/10.8.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/mocha/10.8.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/mocha/10.7.3/10.8.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/mocha/10.7.3/10.8.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
mochajs/mocha (mocha) ### [`v10.8.2`](https://redirect.github.com/mochajs/mocha/blob/HEAD/CHANGELOG.md#1082-2024-10-30) [Compare Source](https://redirect.github.com/mochajs/mocha/compare/v10.8.1...v10.8.2) ##### 🩹 Fixes - support errors with circular dependencies in object values with --parallel ([#​5212](https://redirect.github.com/mochajs/mocha/issues/5212)) ([ba0fefe](https://redirect.github.com/mochajs/mocha/commit/ba0fefe10b08a689cf49edc3818026938aa3a240)) - test link in html reporter ([#​5224](https://redirect.github.com/mochajs/mocha/issues/5224)) ([f054acc](https://redirect.github.com/mochajs/mocha/commit/f054acc1f60714bbe00ad1ab270fb4977836d045)) ##### 📚 Documentation - indicate 'exports' interface does not work in browsers ([#​5181](https://redirect.github.com/mochajs/mocha/issues/5181)) ([14e640e](https://redirect.github.com/mochajs/mocha/commit/14e640ee49718d587779a9594b18f3796c42cf2a)) ##### 🧹 Chores - fix docs builds by re-adding eleventy and ignoring gitignore again ([#​5240](https://redirect.github.com/mochajs/mocha/issues/5240)) ([881e3b0](https://redirect.github.com/mochajs/mocha/commit/881e3b0ca2e24284aab2a04f63639a0aa9e0ad1b)) ##### 🤖 Automation - **deps:** bump the github-actions group with 1 update ([#​5132](https://redirect.github.com/mochajs/mocha/issues/5132)) ([e536ab2](https://redirect.github.com/mochajs/mocha/commit/e536ab25b308774e3103006c044cb996a2e17c87)) ### [`v10.8.1`](https://redirect.github.com/mochajs/mocha/blob/HEAD/CHANGELOG.md#1081-2024-10-29) [Compare Source](https://redirect.github.com/mochajs/mocha/compare/v10.8.0...v10.8.1) ##### 🩹 Fixes - handle case of invalid package.json with no explicit config ([#​5198](https://redirect.github.com/mochajs/mocha/issues/5198)) ([f72bc17](https://redirect.github.com/mochajs/mocha/commit/f72bc17cb44164bcfff7abc83d0d37d99a061104)) - Typos on mochajs.org ([#​5237](https://redirect.github.com/mochajs/mocha/issues/5237)) ([d8ca270](https://redirect.github.com/mochajs/mocha/commit/d8ca270a960554c9d5c5fbf264e89d668d01ff0d)) - use accurate test links in HTML reporter ([#​5228](https://redirect.github.com/mochajs/mocha/issues/5228)) ([68803b6](https://redirect.github.com/mochajs/mocha/commit/68803b685d55dcccc51fa6ccfd27701cda4e26ed)) ### [`v10.8.0`](https://redirect.github.com/mochajs/mocha/blob/HEAD/CHANGELOG.md#1080-2024-10-29) [Compare Source](https://redirect.github.com/mochajs/mocha/compare/v10.7.3...v10.8.0) ##### 🌟 Features - highlight browser failures ([#​5222](https://redirect.github.com/mochajs/mocha/issues/5222)) ([8ff4845](https://redirect.github.com/mochajs/mocha/commit/8ff48453a8b12d9cacf56b0c0c544c8256af64c7)) ##### 🩹 Fixes - remove `:is()` from `mocha.css` to support older browsers ([#​5225](https://redirect.github.com/mochajs/mocha/issues/5225)) ([#​5227](https://redirect.github.com/mochajs/mocha/issues/5227)) ([0a24b58](https://redirect.github.com/mochajs/mocha/commit/0a24b58477ea8ad146afc798930800b02c08790a)) ##### 📚 Documentation - add `SECURITY.md` pointing to Tidelift ([#​5210](https://redirect.github.com/mochajs/mocha/issues/5210)) ([bd7e63a](https://redirect.github.com/mochajs/mocha/commit/bd7e63a1f6d98535ce1ed1ecdb57b3e4db8a33c5)) - adopt Collective Funds Guidelines 0.1 ([#​5199](https://redirect.github.com/mochajs/mocha/issues/5199)) ([2b03d86](https://redirect.github.com/mochajs/mocha/commit/2b03d865eec63d627ff229e07d777f25061260d4)) - update README, LICENSE and fix outdated ([#​5197](https://redirect.github.com/mochajs/mocha/issues/5197)) ([1203e0e](https://redirect.github.com/mochajs/mocha/commit/1203e0ed739bbbf12166078738357fdb29a8c000)) ##### 🧹 Chores - fix npm scripts on windows ([#​5219](https://redirect.github.com/mochajs/mocha/issues/5219)) ([1173da0](https://redirect.github.com/mochajs/mocha/commit/1173da0bf614e8d2a826687802ee8cbe8671ccf1)) - remove trailing whitespace in SECURITY.md ([7563e59](https://redirect.github.com/mochajs/mocha/commit/7563e59ae3c78ada305d26eadb86998ab54342da)) ### [`v10.7.3`](https://redirect.github.com/mochajs/mocha/blob/HEAD/CHANGELOG.md#1073-2024-08-09) [Compare Source](https://redirect.github.com/mochajs/mocha/compare/v10.7.0...v10.7.3) ##### 🩹 Fixes - make release-please build work ([#​5194](https://redirect.github.com/mochajs/mocha/issues/5194)) ([afd66ef](https://redirect.github.com/mochajs/mocha/commit/afd66ef3df20fab51ce38b97216c09108e5c2bfd)) ### [`v10.7.0`](https://redirect.github.com/mochajs/mocha/blob/HEAD/CHANGELOG.md#1070--2024-07-20) [Compare Source](https://redirect.github.com/mochajs/mocha/compare/v10.6.1...v10.7.0) ##### :tada: Enhancements - [#​4771](https://redirect.github.com/mochajs/mocha/pull/4771) feat: add option to not fail on failing test suite ([**@​ilgonmic**](https://redirect.github.com/ilgonmic)) ### [`v10.6.1`](https://redirect.github.com/mochajs/mocha/blob/HEAD/CHANGELOG.md#1061--2024-07-20) [Compare Source](https://redirect.github.com/mochajs/mocha/compare/v10.6.0...v10.6.1) ##### :bug: Fixes - [#​3825](https://redirect.github.com/mochajs/mocha/pull/3825) fix: do not exit when only unref'd timer is present in test code ([**@​boneskull**](https://redirect.github.com/boneskull)) - [#​5040](https://redirect.github.com/mochajs/mocha/pull/5040) fix: support canonical module ([**@​JacobLey**](https://redirect.github.com/JacobLey)) ### [`v10.6.0`](https://redirect.github.com/mochajs/mocha/blob/HEAD/CHANGELOG.md#1060--2024-07-02) [Compare Source](https://redirect.github.com/mochajs/mocha/compare/v10.5.2...v10.6.0) ##### :tada: Enhancements - [#​5150](https://redirect.github.com/mochajs/mocha/pull/5150) feat: allow ^ versions for character encoding packages ([**@​JoshuaKGoldberg**](https://redirect.github.com/JoshuaKGoldberg)) - [#​5151](https://redirect.github.com/mochajs/mocha/pull/5151) feat: allow ^ versions for file matching packages ([**@​JoshuaKGoldberg**](https://redirect.github.com/JoshuaKGoldberg)) - [#​5152](https://redirect.github.com/mochajs/mocha/pull/5152) feat: allow ^ versions for yargs packages ([**@​JoshuaKGoldberg**](https://redirect.github.com/JoshuaKGoldberg)) - [#​5153](https://redirect.github.com/mochajs/mocha/pull/5153) feat: allow ^ versions for data serialization packages ([**@​JoshuaKGoldberg**](https://redirect.github.com/JoshuaKGoldberg)) - [#​5154](https://redirect.github.com/mochajs/mocha/pull/5154) feat: allow ^ versions for miscellaneous packages ([**@​JoshuaKGoldberg**](https://redirect.github.com/JoshuaKGoldberg)) ### [`v10.5.2`](https://redirect.github.com/mochajs/mocha/blob/HEAD/CHANGELOG.md#1052--2024-06-25) [Compare Source](https://redirect.github.com/mochajs/mocha/compare/v10.5.1...v10.5.2) ##### :bug: Fixes - [#​5032](https://redirect.github.com/mochajs/mocha/pull/5032) fix: better tracking of seen objects in error serialization ([**@​sam-super**](https://redirect.github.com/sam-super)) ### [`v10.5.1`](https://redirect.github.com/mochajs/mocha/blob/HEAD/CHANGELOG.md#1051--2024-06-24) [Compare Source](https://redirect.github.com/mochajs/mocha/compare/v10.5.0...v10.5.1) ##### :bug: Fixes - [#​5086](https://redirect.github.com/mochajs/mocha/pull/5086) fix: Add error handling for nonexistent file case with --file option ([**@​khoaHyh**](https://redirect.github.com/khoaHyh)) ### [`v10.5.0`](https://redirect.github.com/mochajs/mocha/blob/HEAD/CHANGELOG.md#1050--2024-06-24) [Compare Source](https://redirect.github.com/mochajs/mocha/compare/v10.4.0...v10.5.0) ##### :tada: Enhancements - [#​5015](https://redirect.github.com/mochajs/mocha/pull/5015) feat: use \ and \ for browser progress indicator instead of \ ([**@​yourWaifu**](https://redirect.github.com/yourWaifu)) - [#​5143](https://redirect.github.com/mochajs/mocha/pull/5143) feat: allow using any 3.x chokidar dependencies ([**@​simhnna**](https://redirect.github.com/simhnna)) - [#​4835](https://redirect.github.com/mochajs/mocha/pull/4835) feat: add MOCHA_OPTIONS env variable ([**@​icholy**](https://redirect.github.com/icholy)) ##### :bug: Fixes - [#​5107](https://redirect.github.com/mochajs/mocha/pull/5107) fix: include stack in browser uncaught error reporting ([**@​JoshuaKGoldberg**](https://redirect.github.com/JoshuaKGoldberg)) ##### :nut_and_bolt: Other - [#​5110](https://redirect.github.com/mochajs/mocha/pull/5110) chore: switch two-column list styles to be opt-in ([**@​marjys**](https://redirect.github.com/marjys)) - [#​5135](https://redirect.github.com/mochajs/mocha/pull/5135) chore: fix some typos in comments ([**@​StevenMia**](https://redirect.github.com/StevenMia)) - [#​5130](https://redirect.github.com/mochajs/mocha/pull/5130) chore: rename 'master' to 'main' in docs and tooling ([**@​JoshuaKGoldberg**](https://redirect.github.com/JoshuaKGoldberg)) ### [`v10.4.0`](https://redirect.github.com/mochajs/mocha/blob/HEAD/CHANGELOG.md#1040--2024-03-26) [Compare Source](https://redirect.github.com/mochajs/mocha/compare/v10.3.0...v10.4.0) ##### :tada: Enhancements - [#​4829](https://redirect.github.com/mochajs/mocha/pull/4829) feat: include `.cause` stacks in the error stack traces ([**@​voxpelli**](https://redirect.github.com/voxpelli)) - [#​4985](https://redirect.github.com/mochajs/mocha/pull/4985) feat: add file path to xunit reporter ([**@​bmish**](https://redirect.github.com/bmish)) ##### :bug: Fixes - [#​5074](https://redirect.github.com/mochajs/mocha/pull/5074) fix: harden error handling in `lib/cli/run.js` ([**@​stalet**](https://redirect.github.com/stalet)) ##### :nut_and_bolt: Other - [#​5077](https://redirect.github.com/mochajs/mocha/pull/5077) chore: add mtfoley/pr-compliance-action ([**@​JoshuaKGoldberg**](https://redirect.github.com/JoshuaKGoldberg)) - [#​5060](https://redirect.github.com/mochajs/mocha/pull/5060) chore: migrate ESLint config to flat config ([**@​JoshuaKGoldberg**](https://redirect.github.com/JoshuaKGoldberg)) - [#​5095](https://redirect.github.com/mochajs/mocha/pull/5095) chore: revert [#​5069](https://redirect.github.com/mochajs/mocha/pull/5069) to restore Netlify builds ([**@​voxpelli**](https://redirect.github.com/voxpelli)) - [#​5097](https://redirect.github.com/mochajs/mocha/pull/5097) docs: add sponsored to sponsorship link rels ([**@​JoshuaKGoldberg**](https://redirect.github.com/JoshuaKGoldberg)) - [#​5093](https://redirect.github.com/mochajs/mocha/pull/5093) chore: add 'status: in triage' label to issue templates and docs ([**@​JoshuaKGoldberg**](https://redirect.github.com/JoshuaKGoldberg)) - [#​5083](https://redirect.github.com/mochajs/mocha/pull/5083) docs: fix CHANGELOG.md headings to start with a root-level h1 ([**@​JoshuaKGoldberg**](https://redirect.github.com/JoshuaKGoldberg)) - [#​5100](https://redirect.github.com/mochajs/mocha/pull/5100) chore: fix header generation and production build crashes ([**@​JoshuaKGoldberg**](https://redirect.github.com/JoshuaKGoldberg)) - [#​5104](https://redirect.github.com/mochajs/mocha/pull/5104) chore: bump ESLint ecmaVersion to 2020 ([**@​JoshuaKGoldberg**](https://redirect.github.com/JoshuaKGoldberg)) - [#​5116](https://redirect.github.com/mochajs/mocha/pull/5116) fix: eleventy template builds crash with 'unexpected token at ": string, msg..."' ([**@​LcsK**](https://redirect.github.com/LcsK)) - [#​4869](https://redirect.github.com/mochajs/mocha/pull/4869) docs: fix documentation concerning glob expansion on UNIX ([**@​binki**](https://redirect.github.com/binki)) - [#​5122](https://redirect.github.com/mochajs/mocha/pull/5122) test: fix xunit integration test ([**@​voxpelli**](https://redirect.github.com/voxpelli)) - [#​5123](https://redirect.github.com/mochajs/mocha/pull/5123) chore: activate dependabot for workflows ([**@​voxpelli**](https://redirect.github.com/voxpelli)) - [#​5125](https://redirect.github.com/mochajs/mocha/pull/5125) build(deps): bump the github-actions group with 2 updates ([**@​dependabot**](https://redirect.github.com/dependabot)) ### [`v10.3.0`](https://redirect.github.com/mochajs/mocha/blob/HEAD/CHANGELOG.md#1030--2024-02-08) [Compare Source](https://redirect.github.com/mochajs/mocha/compare/v10.2.0...v10.3.0) This is a stable release equivalent to [10.30.0-prerelease](#​1030-prerelease--2024-01-18).
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Never, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/TryGhost/Ghost). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index cd4310d1a67..2251bd74de5 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -19,7 +19,7 @@ ], "devDependencies": { "c8": "8.0.1", - "mocha": "10.2.0", + "mocha": "10.8.2", "should": "13.2.3", "sinon": "15.2.0" }, From bb8a613c4494034b8278f0f126a7d665e1b70b05 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Thu, 24 Apr 2025 14:39:47 -0500 Subject: [PATCH 059/118] Bumped packages to use tryghost/errors 1.3.7 (#23039) ref https://github.com/TryGhost/Ghost-Moya/actions/runs/14648874041/job/41110881922 There was an error in the canary builds trying to use errors 1.3.7 but getting 1.3.5. We should have all packages on the same version anyways, so let's see if this resolves it. No clear cause. --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 2251bd74de5..6b60a591735 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@tryghost/debug": "0.1.32", - "@tryghost/errors": "1.3.5", + "@tryghost/errors": "1.3.7", "@tryghost/promise": "0.3.12", "@tryghost/tpl": "0.1.32", "@tryghost/validator": "0.2.14", From 84f32e48a362f6764543c7b9d218027cd8b77446 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 26 Apr 2025 19:42:14 +0100 Subject: [PATCH 060/118] Update TryGhost packages (#20735) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [@tryghost/adapter-base-cache](https://redirect.github.com/TryGhost/SDK) ([source](https://redirect.github.com/TryGhost/SDK/tree/HEAD/packages/adapter-base-cache)) | [`0.1.12` -> `0.1.13`](https://renovatebot.com/diffs/npm/@tryghost%2fadapter-base-cache/0.1.12/0.1.13) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2fadapter-base-cache/0.1.13?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2fadapter-base-cache/0.1.13?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2fadapter-base-cache/0.1.12/0.1.13?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2fadapter-base-cache/0.1.12/0.1.13?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/admin-api-schema](https://redirect.github.com/TryGhost/SDK/tree/main#readme) ([source](https://redirect.github.com/TryGhost/SDK)) | [`4.5.5` -> `4.5.6`](https://renovatebot.com/diffs/npm/@tryghost%2fadmin-api-schema/4.5.5/4.5.6) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2fadmin-api-schema/4.5.6?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2fadmin-api-schema/4.5.6?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2fadmin-api-schema/4.5.5/4.5.6?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2fadmin-api-schema/4.5.5/4.5.6?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/bookshelf-plugins](https://redirect.github.com/TryGhost/framework) ([source](https://redirect.github.com/TryGhost/framework/tree/HEAD/packages/bookshelf-plugins)) | [`0.6.25` -> `0.6.26`](https://renovatebot.com/diffs/npm/@tryghost%2fbookshelf-plugins/0.6.25/0.6.26) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2fbookshelf-plugins/0.6.26?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2fbookshelf-plugins/0.6.26?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2fbookshelf-plugins/0.6.25/0.6.26?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2fbookshelf-plugins/0.6.25/0.6.26?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/color-utils](https://redirect.github.com/TryGhost/SDK/tree/main#readme) ([source](https://redirect.github.com/TryGhost/SDK)) | [`0.2.2` -> `0.2.3`](https://renovatebot.com/diffs/npm/@tryghost%2fcolor-utils/0.2.2/0.2.3) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2fcolor-utils/0.2.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2fcolor-utils/0.2.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2fcolor-utils/0.2.2/0.2.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2fcolor-utils/0.2.2/0.2.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/config-url-helpers](https://redirect.github.com/TryGhost/SDK/tree/main#readme) ([source](https://redirect.github.com/TryGhost/SDK)) | [`1.0.12` -> `1.0.13`](https://renovatebot.com/diffs/npm/@tryghost%2fconfig-url-helpers/1.0.12/1.0.13) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2fconfig-url-helpers/1.0.13?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2fconfig-url-helpers/1.0.13?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2fconfig-url-helpers/1.0.12/1.0.13?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2fconfig-url-helpers/1.0.12/1.0.13?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/content-api](https://redirect.github.com/TryGhost/SDK/tree/main#readme) ([source](https://redirect.github.com/TryGhost/SDK)) | [`1.11.21` -> `1.11.22`](https://renovatebot.com/diffs/npm/@tryghost%2fcontent-api/1.11.21/1.11.22) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2fcontent-api/1.11.22?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2fcontent-api/1.11.22?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2fcontent-api/1.11.21/1.11.22?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2fcontent-api/1.11.21/1.11.22?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/database-info](https://redirect.github.com/TryGhost/framework) ([source](https://redirect.github.com/TryGhost/framework/tree/HEAD/packages/database-info)) | [`0.3.27` -> `0.3.29`](https://renovatebot.com/diffs/npm/@tryghost%2fdatabase-info/0.3.27/0.3.29) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2fdatabase-info/0.3.29?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2fdatabase-info/0.3.29?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2fdatabase-info/0.3.27/0.3.29?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2fdatabase-info/0.3.27/0.3.29?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/debug](https://redirect.github.com/TryGhost/framework) ([source](https://redirect.github.com/TryGhost/framework/tree/HEAD/packages/debug)) | [`0.1.32` -> `0.1.34`](https://renovatebot.com/diffs/npm/@tryghost%2fdebug/0.1.32/0.1.34) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2fdebug/0.1.34?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2fdebug/0.1.34?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2fdebug/0.1.32/0.1.34?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2fdebug/0.1.32/0.1.34?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/email-mock-receiver](https://redirect.github.com/TryGhost/framework) ([source](https://redirect.github.com/TryGhost/framework/tree/HEAD/packages/email-mock-receiver)) | [`0.3.8` -> `0.3.10`](https://renovatebot.com/diffs/npm/@tryghost%2femail-mock-receiver/0.3.8/0.3.10) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2femail-mock-receiver/0.3.10?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2femail-mock-receiver/0.3.10?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2femail-mock-receiver/0.3.8/0.3.10?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2femail-mock-receiver/0.3.8/0.3.10?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/express-test](https://redirect.github.com/TryGhost/framework) ([source](https://redirect.github.com/TryGhost/framework/tree/HEAD/packages/express-test)) | [`0.13.15` -> `0.13.17`](https://renovatebot.com/diffs/npm/@tryghost%2fexpress-test/0.13.15/0.13.17) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2fexpress-test/0.13.17?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2fexpress-test/0.13.17?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2fexpress-test/0.13.15/0.13.17?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2fexpress-test/0.13.15/0.13.17?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/helpers](https://redirect.github.com/TryGhost/SDK/tree/main#readme) ([source](https://redirect.github.com/TryGhost/SDK)) | [`1.1.90` -> `1.1.91`](https://renovatebot.com/diffs/npm/@tryghost%2fhelpers/1.1.90/1.1.91) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2fhelpers/1.1.91?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2fhelpers/1.1.91?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2fhelpers/1.1.90/1.1.91?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2fhelpers/1.1.90/1.1.91?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/html-to-mobiledoc](https://redirect.github.com/TryGhost/Koenig) ([source](https://redirect.github.com/TryGhost/Koenig/tree/HEAD/packages/html-to-mobiledoc)) | [`3.1.3` -> `3.2.1`](https://renovatebot.com/diffs/npm/@tryghost%2fhtml-to-mobiledoc/3.1.3/3.2.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2fhtml-to-mobiledoc/3.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2fhtml-to-mobiledoc/3.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2fhtml-to-mobiledoc/3.1.3/3.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2fhtml-to-mobiledoc/3.1.3/3.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/http-cache-utils](https://redirect.github.com/TryGhost/framework) ([source](https://redirect.github.com/TryGhost/framework/tree/HEAD/packages/http-cache-utils)) | [`0.1.17` -> `0.1.19`](https://renovatebot.com/diffs/npm/@tryghost%2fhttp-cache-utils/0.1.17/0.1.19) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2fhttp-cache-utils/0.1.19?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2fhttp-cache-utils/0.1.19?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2fhttp-cache-utils/0.1.17/0.1.19?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2fhttp-cache-utils/0.1.17/0.1.19?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/image-transform](https://redirect.github.com/TryGhost/SDK/tree/main#readme) ([source](https://redirect.github.com/TryGhost/SDK)) | [`1.3.0` -> `1.3.1`](https://renovatebot.com/diffs/npm/@tryghost%2fimage-transform/1.3.0/1.3.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2fimage-transform/1.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2fimage-transform/1.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2fimage-transform/1.3.0/1.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2fimage-transform/1.3.0/1.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/limit-service](https://redirect.github.com/TryGhost/SDK/tree/main#readme) ([source](https://redirect.github.com/TryGhost/SDK)) | [`1.2.14` -> `1.2.15`](https://renovatebot.com/diffs/npm/@tryghost%2flimit-service/1.2.14/1.2.15) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2flimit-service/1.2.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2flimit-service/1.2.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2flimit-service/1.2.14/1.2.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2flimit-service/1.2.14/1.2.15?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/nodemailer](https://redirect.github.com/TryGhost/framework) ([source](https://redirect.github.com/TryGhost/framework/tree/HEAD/packages/nodemailer)) | [`0.3.45` -> `0.3.47`](https://renovatebot.com/diffs/npm/@tryghost%2fnodemailer/0.3.45/0.3.47) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2fnodemailer/0.3.47?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2fnodemailer/0.3.47?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2fnodemailer/0.3.45/0.3.47?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2fnodemailer/0.3.45/0.3.47?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/pretty-cli](https://redirect.github.com/TryGhost/framework) ([source](https://redirect.github.com/TryGhost/framework/tree/HEAD/packages/pretty-cli)) | [`1.2.44` -> `1.2.46`](https://renovatebot.com/diffs/npm/@tryghost%2fpretty-cli/1.2.44/1.2.46) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2fpretty-cli/1.2.46?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2fpretty-cli/1.2.46?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2fpretty-cli/1.2.44/1.2.46?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2fpretty-cli/1.2.44/1.2.46?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/promise](https://redirect.github.com/TryGhost/framework) ([source](https://redirect.github.com/TryGhost/framework/tree/HEAD/packages/promise)) | [`0.3.12` -> `0.3.14`](https://renovatebot.com/diffs/npm/@tryghost%2fpromise/0.3.12/0.3.14) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2fpromise/0.3.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2fpromise/0.3.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2fpromise/0.3.12/0.3.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2fpromise/0.3.12/0.3.14?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/request](https://redirect.github.com/TryGhost/framework) ([source](https://redirect.github.com/TryGhost/framework/tree/HEAD/packages/request)) | [`1.0.8` -> `1.0.10`](https://renovatebot.com/diffs/npm/@tryghost%2frequest/1.0.8/1.0.10) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2frequest/1.0.10?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2frequest/1.0.10?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2frequest/1.0.8/1.0.10?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2frequest/1.0.8/1.0.10?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/root-utils](https://redirect.github.com/TryGhost/framework) ([source](https://redirect.github.com/TryGhost/framework/tree/HEAD/packages/root-utils)) | [`0.3.30` -> `0.3.32`](https://renovatebot.com/diffs/npm/@tryghost%2froot-utils/0.3.30/0.3.32) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2froot-utils/0.3.32?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2froot-utils/0.3.32?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2froot-utils/0.3.30/0.3.32?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2froot-utils/0.3.30/0.3.32?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/string](https://redirect.github.com/TryGhost/SDK/tree/main#readme) ([source](https://redirect.github.com/TryGhost/SDK)) | [`0.2.12` -> `0.2.13`](https://renovatebot.com/diffs/npm/@tryghost%2fstring/0.2.12/0.2.13) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2fstring/0.2.13?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2fstring/0.2.13?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2fstring/0.2.12/0.2.13?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2fstring/0.2.12/0.2.13?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/timezone-data](https://redirect.github.com/TryGhost/SDK/tree/main#readme) ([source](https://redirect.github.com/TryGhost/SDK)) | [`0.4.4` -> `0.4.5`](https://renovatebot.com/diffs/npm/@tryghost%2ftimezone-data/0.4.4/0.4.5) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2ftimezone-data/0.4.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2ftimezone-data/0.4.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2ftimezone-data/0.4.4/0.4.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2ftimezone-data/0.4.4/0.4.5?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/tpl](https://redirect.github.com/TryGhost/framework) ([source](https://redirect.github.com/TryGhost/framework/tree/HEAD/packages/tpl)) | [`0.1.32` -> `0.1.34`](https://renovatebot.com/diffs/npm/@tryghost%2ftpl/0.1.32/0.1.34) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2ftpl/0.1.34?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2ftpl/0.1.34?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2ftpl/0.1.32/0.1.34?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2ftpl/0.1.32/0.1.34?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/url-utils](https://redirect.github.com/TryGhost/SDK/tree/main#readme) ([source](https://redirect.github.com/TryGhost/SDK)) | [`4.4.8` -> `4.4.9`](https://renovatebot.com/diffs/npm/@tryghost%2furl-utils/4.4.8/4.4.9) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2furl-utils/4.4.9?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2furl-utils/4.4.9?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2furl-utils/4.4.8/4.4.9?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2furl-utils/4.4.8/4.4.9?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/validator](https://redirect.github.com/TryGhost/framework) ([source](https://redirect.github.com/TryGhost/framework/tree/HEAD/packages/validator)) | [`0.2.14` -> `0.2.16`](https://renovatebot.com/diffs/npm/@tryghost%2fvalidator/0.2.14/0.2.16) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2fvalidator/0.2.16?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2fvalidator/0.2.16?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2fvalidator/0.2.14/0.2.16?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2fvalidator/0.2.14/0.2.16?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/version](https://redirect.github.com/TryGhost/framework) ([source](https://redirect.github.com/TryGhost/framework/tree/HEAD/packages/version)) | [`0.1.30` -> `0.1.32`](https://renovatebot.com/diffs/npm/@tryghost%2fversion/0.1.30/0.1.32) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2fversion/0.1.32?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2fversion/0.1.32?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2fversion/0.1.30/0.1.32?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2fversion/0.1.30/0.1.32?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/webhook-mock-receiver](https://redirect.github.com/TryGhost/framework) ([source](https://redirect.github.com/TryGhost/framework/tree/HEAD/packages/webhook-mock-receiver)) | [`0.2.14` -> `0.2.16`](https://renovatebot.com/diffs/npm/@tryghost%2fwebhook-mock-receiver/0.2.14/0.2.16) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2fwebhook-mock-receiver/0.2.16?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2fwebhook-mock-receiver/0.2.16?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2fwebhook-mock-receiver/0.2.14/0.2.16?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2fwebhook-mock-receiver/0.2.14/0.2.16?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [@tryghost/zip](https://redirect.github.com/TryGhost/framework) ([source](https://redirect.github.com/TryGhost/framework/tree/HEAD/packages/zip)) | [`1.1.46` -> `1.1.48`](https://renovatebot.com/diffs/npm/@tryghost%2fzip/1.1.46/1.1.48) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@tryghost%2fzip/1.1.48?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tryghost%2fzip/1.1.48?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tryghost%2fzip/1.1.46/1.1.48?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tryghost%2fzip/1.1.46/1.1.48?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
TryGhost/SDK (@​tryghost/adapter-base-cache) ### [`v0.1.13`](https://redirect.github.com/TryGhost/SDK/compare/@tryghost/adapter-base-cache@0.1.12...@tryghost/adapter-base-cache@0.1.13) [Compare Source](https://redirect.github.com/TryGhost/SDK/compare/@tryghost/adapter-base-cache@0.1.12...@tryghost/adapter-base-cache@0.1.13)
TryGhost/SDK (@​tryghost/admin-api-schema) ### [`v4.5.6`](https://redirect.github.com/TryGhost/SDK/compare/@tryghost/admin-api-schema@4.5.5...@tryghost/admin-api-schema@4.5.6) [Compare Source](https://redirect.github.com/TryGhost/SDK/compare/@tryghost/admin-api-schema@4.5.5...@tryghost/admin-api-schema@4.5.6)
TryGhost/framework (@​tryghost/bookshelf-plugins) ### [`v0.6.26`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/bookshelf-plugins@0.6.25...@tryghost/bookshelf-plugins@0.6.26) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/bookshelf-plugins@0.6.25...@tryghost/bookshelf-plugins@0.6.26)
TryGhost/framework (@​tryghost/database-info) ### [`v0.3.29`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/database-info@0.3.28...@tryghost/database-info@0.3.29) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/database-info@0.3.28...@tryghost/database-info@0.3.29) ### [`v0.3.28`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/database-info@0.3.27...@tryghost/database-info@0.3.28) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/database-info@0.3.27...@tryghost/database-info@0.3.28)
TryGhost/framework (@​tryghost/debug) ### [`v0.1.34`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/debug@0.1.33...@tryghost/debug@0.1.34) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/debug@0.1.33...@tryghost/debug@0.1.34) ### [`v0.1.33`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/debug@0.1.32...@tryghost/debug@0.1.33) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/debug@0.1.32...@tryghost/debug@0.1.33)
TryGhost/framework (@​tryghost/email-mock-receiver) ### [`v0.3.10`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/email-mock-receiver@0.3.9...@tryghost/email-mock-receiver@0.3.10) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/email-mock-receiver@0.3.9...@tryghost/email-mock-receiver@0.3.10) ### [`v0.3.9`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/email-mock-receiver@0.3.8...@tryghost/email-mock-receiver@0.3.9) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/email-mock-receiver@0.3.8...@tryghost/email-mock-receiver@0.3.9)
TryGhost/framework (@​tryghost/express-test) ### [`v0.13.17`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/express-test@0.13.16...@tryghost/express-test@0.13.17) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/express-test@0.13.16...@tryghost/express-test@0.13.17) ### [`v0.13.16`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/express-test@0.13.15...@tryghost/express-test@0.13.16) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/express-test@0.13.15...@tryghost/express-test@0.13.16)
TryGhost/Koenig (@​tryghost/html-to-mobiledoc) ### [`v3.2.1`](https://redirect.github.com/TryGhost/Koenig/compare/@tryghost/html-to-mobiledoc@3.2.0...@tryghost/html-to-mobiledoc@3.2.1) [Compare Source](https://redirect.github.com/TryGhost/Koenig/compare/@tryghost/html-to-mobiledoc@3.2.0...@tryghost/html-to-mobiledoc@3.2.1) ### [`v3.2.0`](https://redirect.github.com/TryGhost/Koenig/compare/@tryghost/html-to-mobiledoc@3.1.6...@tryghost/html-to-mobiledoc@3.2.0) [Compare Source](https://redirect.github.com/TryGhost/Koenig/compare/@tryghost/html-to-mobiledoc@3.1.6...@tryghost/html-to-mobiledoc@3.2.0) ### [`v3.1.6`](https://redirect.github.com/TryGhost/Koenig/compare/@tryghost/html-to-mobiledoc@3.1.5...@tryghost/html-to-mobiledoc@3.1.6) [Compare Source](https://redirect.github.com/TryGhost/Koenig/compare/@tryghost/html-to-mobiledoc@3.1.5...@tryghost/html-to-mobiledoc@3.1.6) ### [`v3.1.5`](https://redirect.github.com/TryGhost/Koenig/compare/@tryghost/html-to-mobiledoc@3.1.4...@tryghost/html-to-mobiledoc@3.1.5) [Compare Source](https://redirect.github.com/TryGhost/Koenig/compare/@tryghost/html-to-mobiledoc@3.1.4...@tryghost/html-to-mobiledoc@3.1.5) ### [`v3.1.4`](https://redirect.github.com/TryGhost/Koenig/compare/@tryghost/html-to-mobiledoc@3.1.3...@tryghost/html-to-mobiledoc@3.1.4) [Compare Source](https://redirect.github.com/TryGhost/Koenig/compare/@tryghost/html-to-mobiledoc@3.1.3...@tryghost/html-to-mobiledoc@3.1.4)
TryGhost/framework (@​tryghost/http-cache-utils) ### [`v0.1.19`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/http-cache-utils@0.1.18...@tryghost/http-cache-utils@0.1.19) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/http-cache-utils@0.1.18...@tryghost/http-cache-utils@0.1.19) ### [`v0.1.18`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/http-cache-utils@0.1.17...@tryghost/http-cache-utils@0.1.18) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/http-cache-utils@0.1.17...@tryghost/http-cache-utils@0.1.18)
TryGhost/framework (@​tryghost/nodemailer) ### [`v0.3.47`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/nodemailer@0.3.46...@tryghost/nodemailer@0.3.47) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/nodemailer@0.3.46...@tryghost/nodemailer@0.3.47) ### [`v0.3.46`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/nodemailer@0.3.45...@tryghost/nodemailer@0.3.46) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/nodemailer@0.3.45...@tryghost/nodemailer@0.3.46)
TryGhost/framework (@​tryghost/pretty-cli) ### [`v1.2.46`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/pretty-cli@1.2.45...@tryghost/pretty-cli@1.2.46) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/pretty-cli@1.2.45...@tryghost/pretty-cli@1.2.46) ### [`v1.2.45`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/pretty-cli@1.2.44...@tryghost/pretty-cli@1.2.45) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/pretty-cli@1.2.44...@tryghost/pretty-cli@1.2.45)
TryGhost/framework (@​tryghost/promise) ### [`v0.3.14`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/promise@0.3.13...@tryghost/promise@0.3.14) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/promise@0.3.13...@tryghost/promise@0.3.14) ### [`v0.3.13`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/promise@0.3.12...@tryghost/promise@0.3.13) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/promise@0.3.12...@tryghost/promise@0.3.13)
TryGhost/framework (@​tryghost/request) ### [`v1.0.10`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/request@1.0.9...@tryghost/request@1.0.10) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/request@1.0.9...@tryghost/request@1.0.10) ### [`v1.0.9`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/request@1.0.8...@tryghost/request@1.0.9) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/request@1.0.8...@tryghost/request@1.0.9)
TryGhost/framework (@​tryghost/root-utils) ### [`v0.3.32`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/root-utils@0.3.31...@tryghost/root-utils@0.3.32) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/root-utils@0.3.31...@tryghost/root-utils@0.3.32) ### [`v0.3.31`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/root-utils@0.3.30...@tryghost/root-utils@0.3.31) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/root-utils@0.3.30...@tryghost/root-utils@0.3.31)
TryGhost/framework (@​tryghost/tpl) ### [`v0.1.34`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/tpl@0.1.33...@tryghost/tpl@0.1.34) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/tpl@0.1.33...@tryghost/tpl@0.1.34) ### [`v0.1.33`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/tpl@0.1.32...@tryghost/tpl@0.1.33) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/tpl@0.1.32...@tryghost/tpl@0.1.33)
TryGhost/framework (@​tryghost/validator) ### [`v0.2.16`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/validator@0.2.15...@tryghost/validator@0.2.16) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/validator@0.2.15...@tryghost/validator@0.2.16) ### [`v0.2.15`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/validator@0.2.14...@tryghost/validator@0.2.15) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/validator@0.2.14...@tryghost/validator@0.2.15)
TryGhost/framework (@​tryghost/version) ### [`v0.1.32`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/version@0.1.31...@tryghost/version@0.1.32) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/version@0.1.31...@tryghost/version@0.1.32) ### [`v0.1.31`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/version@0.1.30...@tryghost/version@0.1.31) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/version@0.1.30...@tryghost/version@0.1.31)
TryGhost/framework (@​tryghost/webhook-mock-receiver) ### [`v0.2.16`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/webhook-mock-receiver@0.2.15...@tryghost/webhook-mock-receiver@0.2.16) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/webhook-mock-receiver@0.2.15...@tryghost/webhook-mock-receiver@0.2.16) ### [`v0.2.15`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/webhook-mock-receiver@0.2.14...@tryghost/webhook-mock-receiver@0.2.15) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/webhook-mock-receiver@0.2.14...@tryghost/webhook-mock-receiver@0.2.15)
TryGhost/framework (@​tryghost/zip) ### [`v1.1.48`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/zip@1.1.47...@tryghost/zip@1.1.48) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/zip@1.1.47...@tryghost/zip@1.1.48) ### [`v1.1.47`](https://redirect.github.com/TryGhost/framework/compare/@tryghost/zip@1.1.46...@tryghost/zip@1.1.47) [Compare Source](https://redirect.github.com/TryGhost/framework/compare/@tryghost/zip@1.1.46...@tryghost/zip@1.1.47)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - Between 05:00 PM and 11:59 PM, only on Friday ( * 17-23 * * 5 ), Only on Sunday and Saturday ( * * * * 0,6 ), Between 12:00 AM and 12:59 PM, only on Monday ( * 0-12 * * 1 ) (UTC). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Never, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/TryGhost/Ghost). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- ghost/api-framework/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 6b60a591735..36949fc7544 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -24,11 +24,11 @@ "sinon": "15.2.0" }, "dependencies": { - "@tryghost/debug": "0.1.32", + "@tryghost/debug": "0.1.34", "@tryghost/errors": "1.3.7", - "@tryghost/promise": "0.3.12", - "@tryghost/tpl": "0.1.32", - "@tryghost/validator": "0.2.14", + "@tryghost/promise": "0.3.14", + "@tryghost/tpl": "0.1.34", + "@tryghost/validator": "0.2.16", "jsonpath": "1.1.1", "lodash": "4.17.21" } From 9175f5522d84795ac8c8f8deedb79d16b04c8685 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Tue, 13 May 2025 14:12:43 +0200 Subject: [PATCH 061/118] Prepared packages for initial publishing ref https://linear.app/ghost/issue/ENG-2036 - allows for publishing publicly and sets the version to 0.9.0, so Lerna will allow us to publish majors for the first release --- ghost/api-framework/package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 36949fc7544..c9377623d74 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,9 +1,11 @@ { "name": "@tryghost/api-framework", - "version": "0.0.0", + "version": "0.9.0", "repository": "https://github.com/TryGhost/Ghost/tree/main/packages/api-framework", "author": "Ghost Foundation", - "private": true, + "publishConfig": { + "access": "public" + }, "main": "index.js", "scripts": { "dev": "echo \"Implement me!\"", From caa6922e9479736e164c3f1ed44f5dab2399a47a Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Tue, 13 May 2025 14:17:03 +0200 Subject: [PATCH 062/118] Published new versions - @tryghost/api-framework@1.0.0 - @tryghost/domain-events@1.0.0 - @tryghost/job-manager@1.0.0 - @tryghost/mw-error-handler@1.0.0 - @tryghost/mw-vhost@1.0.0 - @tryghost/security@1.0.0 --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index c9377623d74..e2e7f98c08e 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/api-framework", - "version": "0.9.0", + "version": "1.0.0", "repository": "https://github.com/TryGhost/Ghost/tree/main/packages/api-framework", "author": "Ghost Foundation", "publishConfig": { From fb1ea3d97ab6f0aac961874dc5e51b8edaa9d72c Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Tue, 13 May 2025 14:18:26 +0200 Subject: [PATCH 063/118] Added missing dependency - api-framework requires json-stable-stringify but we weren't pulling the dependency in --- ghost/api-framework/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index e2e7f98c08e..2a2a33abfd4 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -31,6 +31,7 @@ "@tryghost/promise": "0.3.14", "@tryghost/tpl": "0.1.34", "@tryghost/validator": "0.2.16", + "json-stable-stringify": "1.3.0", "jsonpath": "1.1.1", "lodash": "4.17.21" } From 5849ef5f60a42c9b445fed7360d1f118cdb8d64d Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Tue, 13 May 2025 14:23:51 +0200 Subject: [PATCH 064/118] Published new versions - @tryghost/api-framework@1.0.1 --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 2a2a33abfd4..57b7579780a 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/api-framework", - "version": "1.0.0", + "version": "1.0.1", "repository": "https://github.com/TryGhost/Ghost/tree/main/packages/api-framework", "author": "Ghost Foundation", "publishConfig": { From 3bd0627bfabb197300b31e827fb1b2dfb4f4b385 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Sat, 17 May 2025 11:06:15 +0100 Subject: [PATCH 065/118] Updated repository references - These packages were recently moved out of the Ghost repo but the repository ref hadn't yet been updated - We do this so it's easier to find the repo where to code lives --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 57b7579780a..63646e633b0 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,7 +1,7 @@ { "name": "@tryghost/api-framework", "version": "1.0.1", - "repository": "https://github.com/TryGhost/Ghost/tree/main/packages/api-framework", + "repository": "https://github.com/TryGhost/framework/tree/main/packages/api-framework", "author": "Ghost Foundation", "publishConfig": { "access": "public" From 9cc67758d54f89efa29ef28a0e80c28aac10d3fc Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Sun, 25 May 2025 18:37:31 +0100 Subject: [PATCH 066/118] Updated repository in package.json - This is a more modern / up-to-date structure for repository - It's correct according to package.json docs (unlike using a URL directly) - I'm updating this where I find it - for consistency --- ghost/api-framework/package.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 63646e633b0..946cf537822 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,7 +1,11 @@ { "name": "@tryghost/api-framework", "version": "1.0.1", - "repository": "https://github.com/TryGhost/framework/tree/main/packages/api-framework", + "repository": { + "type": "git", + "url": "git+https://github.com/TryGhost/framework.git", + "directory": "packages/api-framework" + }, "author": "Ghost Foundation", "publishConfig": { "access": "public" From 55c0872bd122555cce64654f67a1624e479676f9 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Sun, 25 May 2025 21:40:29 +0100 Subject: [PATCH 067/118] Published new versions - @tryghost/api-framework@1.0.2 - @tryghost/bookshelf-collision@0.1.48 - @tryghost/bookshelf-custom-query@0.1.30 - @tryghost/bookshelf-eager-load@0.1.34 - @tryghost/bookshelf-filter@0.5.23 - @tryghost/bookshelf-has-posts@0.1.35 - @tryghost/bookshelf-include-count@0.3.18 - @tryghost/bookshelf-order@0.1.30 - @tryghost/bookshelf-pagination@0.1.51 - @tryghost/bookshelf-plugins@0.6.27 - @tryghost/bookshelf-search@0.1.30 - @tryghost/bookshelf-transaction-events@0.2.19 - @tryghost/config@0.2.27 - @tryghost/database-info@0.3.30 - @tryghost/debug@0.1.35 - @tryghost/domain-events@1.0.1 - @tryghost/elasticsearch@3.0.24 - @tryghost/email-mock-receiver@0.3.11 - @tryghost/errors@1.3.8 - @tryghost/express-test@0.14.0 - @tryghost/http-cache-utils@0.1.20 - @tryghost/http-stream@0.1.36 - @tryghost/jest-snapshot@0.5.18 - @tryghost/job-manager@1.0.1 - @tryghost/logging@2.4.22 - @tryghost/metrics@1.0.38 - @tryghost/mw-error-handler@1.0.7 - @tryghost/mw-vhost@1.0.1 - @tryghost/nodemailer@0.3.48 - @tryghost/pretty-cli@1.2.47 - @tryghost/pretty-stream@0.2.0 - @tryghost/prometheus-metrics@1.0.1 - @tryghost/promise@0.3.15 - @tryghost/request@1.0.11 - @tryghost/root-utils@0.3.33 - @tryghost/security@1.0.1 - @tryghost/server@0.1.52 - @tryghost/tpl@0.1.35 - @tryghost/validator@0.2.17 - @tryghost/version@0.1.33 - @tryghost/webhook-mock-receiver@0.2.17 - @tryghost/zip@1.1.49 --- ghost/api-framework/package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 946cf537822..d2c7395067b 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/api-framework", - "version": "1.0.1", + "version": "1.0.2", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", @@ -30,11 +30,11 @@ "sinon": "15.2.0" }, "dependencies": { - "@tryghost/debug": "0.1.34", - "@tryghost/errors": "1.3.7", - "@tryghost/promise": "0.3.14", - "@tryghost/tpl": "0.1.34", - "@tryghost/validator": "0.2.16", + "@tryghost/debug": "^0.1.35", + "@tryghost/errors": "^1.3.8", + "@tryghost/promise": "^0.3.15", + "@tryghost/tpl": "^0.1.35", + "@tryghost/validator": "^0.2.17", "json-stable-stringify": "1.3.0", "jsonpath": "1.1.1", "lodash": "4.17.21" From d33608dbf24440474fe330108197d3890fcb3170 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 10:36:19 -0600 Subject: [PATCH 068/118] Update dependency lodash to v4.17.23 [SECURITY] (#346) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index d2c7395067b..eaa2172a325 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -37,6 +37,6 @@ "@tryghost/validator": "^0.2.17", "json-stable-stringify": "1.3.0", "jsonpath": "1.1.1", - "lodash": "4.17.21" + "lodash": "4.17.23" } } From eb0d97976e59f4d605634dff1bb8bb02c789eb50 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 10:41:06 -0600 Subject: [PATCH 069/118] Update dependency sinon to v21 (#212) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index eaa2172a325..cbd13bf889e 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -27,7 +27,7 @@ "c8": "8.0.1", "mocha": "10.8.2", "should": "13.2.3", - "sinon": "15.2.0" + "sinon": "21.0.1" }, "dependencies": { "@tryghost/debug": "^0.1.35", From 5ecec0bf04e3713be582df854bf19ada23162509 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 10:46:59 -0600 Subject: [PATCH 070/118] Update dependency mocha to v11 (#174) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index cbd13bf889e..6787f737cc1 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -25,7 +25,7 @@ ], "devDependencies": { "c8": "8.0.1", - "mocha": "10.8.2", + "mocha": "11.7.5", "should": "13.2.3", "sinon": "21.0.1" }, From b7f5b1a94ee41a97c6615ff9e01498809f12d88b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 06:41:38 +0000 Subject: [PATCH 071/118] fix(deps): update dependency jsonpath to v1.2.0 (#357) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 6787f737cc1..415c131cf4e 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -36,7 +36,7 @@ "@tryghost/tpl": "^0.1.35", "@tryghost/validator": "^0.2.17", "json-stable-stringify": "1.3.0", - "jsonpath": "1.1.1", + "jsonpath": "1.2.0", "lodash": "4.17.23" } } From 9752e2dc6678f21196210682b3b468ff2833c030 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 02:59:34 +0000 Subject: [PATCH 072/118] fix(deps): update dependency jsonpath to v1.2.1 (#359) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 415c131cf4e..e83a72f2d97 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -36,7 +36,7 @@ "@tryghost/tpl": "^0.1.35", "@tryghost/validator": "^0.2.17", "json-stable-stringify": "1.3.0", - "jsonpath": "1.2.0", + "jsonpath": "1.2.1", "lodash": "4.17.23" } } From a3bd6b5585951bfabb0a1eaa96ee5c19f9018de4 Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Wed, 11 Feb 2026 11:46:12 -0300 Subject: [PATCH 073/118] Removed jsonpath dependency from api-framework MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit no-issue The only usage was in the add() input validator, where jsonpath.query() checked whether a key existed on a request body object. Every caller in Ghost passes simple top-level property names (e.g. "email", "name", "post_id") — no dotted paths or JSONPath expressions are used anywhere. The existing nil check on the next line already used bracket notation (obj[key]), which would not work with nested paths either, confirming nested key support was never part of the design. Replaced with Object.prototype.hasOwnProperty.call(), which is equivalent for flat property names and has no external dependencies. --- ghost/api-framework/lib/validators/input/all.js | 4 +--- ghost/api-framework/package.json | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/ghost/api-framework/lib/validators/input/all.js b/ghost/api-framework/lib/validators/input/all.js index fadb17bb2c8..661600db54d 100644 --- a/ghost/api-framework/lib/validators/input/all.js +++ b/ghost/api-framework/lib/validators/input/all.js @@ -146,14 +146,12 @@ module.exports = { } } - const jsonpath = require('jsonpath'); - if (apiConfig.data) { const missedDataProperties = []; const nilDataProperties = []; _.each(apiConfig.data, (value, key) => { - if (jsonpath.query(frame.data[apiConfig.docName][0], key).length === 0) { + if (!Object.prototype.hasOwnProperty.call(frame.data[apiConfig.docName][0], key)) { missedDataProperties.push(key); } else if (_.isNil(frame.data[apiConfig.docName][0][key])) { nilDataProperties.push(key); diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index e83a72f2d97..7b7e19d5bea 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -36,7 +36,6 @@ "@tryghost/tpl": "^0.1.35", "@tryghost/validator": "^0.2.17", "json-stable-stringify": "1.3.0", - "jsonpath": "1.2.1", "lodash": "4.17.23" } } From 5cc63fa25f2eae0a10362ec767aab3bd2964666b Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Thu, 12 Feb 2026 13:32:46 -0300 Subject: [PATCH 074/118] Published new versions - @tryghost/api-framework@1.0.3 - @tryghost/bookshelf-collision@0.1.49 - @tryghost/bookshelf-custom-query@0.1.31 - @tryghost/bookshelf-eager-load@0.1.35 - @tryghost/bookshelf-filter@0.5.24 - @tryghost/bookshelf-has-posts@0.1.36 - @tryghost/bookshelf-include-count@0.3.19 - @tryghost/bookshelf-order@0.1.31 - @tryghost/bookshelf-pagination@0.1.52 - @tryghost/bookshelf-plugins@0.6.28 - @tryghost/bookshelf-search@0.1.31 - @tryghost/bookshelf-transaction-events@0.2.20 - @tryghost/config@0.2.28 - @tryghost/database-info@0.3.31 - @tryghost/debug@0.1.36 - @tryghost/domain-events@1.0.4 - @tryghost/elasticsearch@3.0.25 - @tryghost/email-mock-receiver@0.3.12 - @tryghost/errors@1.3.9 - @tryghost/express-test@0.15.1 - @tryghost/http-cache-utils@0.1.21 - @tryghost/http-stream@0.1.38 - @tryghost/jest-snapshot@0.5.19 - @tryghost/job-manager@1.0.5 - @tryghost/logging@2.5.1 - @tryghost/metrics@1.0.39 - @tryghost/mw-error-handler@1.0.9 - @tryghost/mw-vhost@1.0.2 - @tryghost/nodemailer@0.3.49 - @tryghost/pretty-cli@1.2.48 - @tryghost/pretty-stream@0.2.1 - @tryghost/prometheus-metrics@1.0.4 - @tryghost/promise@0.3.16 - @tryghost/request@1.0.13 - @tryghost/root-utils@0.3.34 - @tryghost/security@1.0.2 - @tryghost/server@0.1.55 - @tryghost/tpl@0.1.36 - @tryghost/validator@0.2.18 - @tryghost/version@0.1.34 - @tryghost/webhook-mock-receiver@0.2.18 - @tryghost/zip@1.1.50 --- ghost/api-framework/package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 7b7e19d5bea..4c24b26bbcc 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/api-framework", - "version": "1.0.2", + "version": "1.0.3", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", @@ -30,11 +30,11 @@ "sinon": "21.0.1" }, "dependencies": { - "@tryghost/debug": "^0.1.35", - "@tryghost/errors": "^1.3.8", - "@tryghost/promise": "^0.3.15", - "@tryghost/tpl": "^0.1.35", - "@tryghost/validator": "^0.2.17", + "@tryghost/debug": "^0.1.36", + "@tryghost/errors": "^1.3.9", + "@tryghost/promise": "^0.3.16", + "@tryghost/tpl": "^0.1.36", + "@tryghost/validator": "^0.2.18", "json-stable-stringify": "1.3.0", "lodash": "4.17.23" } From c8abd24d66c56d4d86ce0a69a0f15da5689a52bd Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Fri, 20 Feb 2026 13:25:54 -0600 Subject: [PATCH 075/118] Published new versions - project: @tryghost/bookshelf-transaction-events 0.2.21 - project: @tryghost/bookshelf-include-count 0.3.20 - project: @tryghost/bookshelf-custom-query 0.1.32 - project: @tryghost/webhook-mock-receiver 0.2.19 - project: @tryghost/bookshelf-eager-load 0.1.36 - project: @tryghost/bookshelf-pagination 0.1.54 - project: @tryghost/bookshelf-collision 0.1.50 - project: @tryghost/bookshelf-has-posts 0.1.37 - project: @tryghost/email-mock-receiver 0.3.13 - project: @tryghost/prometheus-metrics 1.0.5 - project: @tryghost/bookshelf-plugins 0.6.30 - project: @tryghost/bookshelf-filter 0.5.25 - project: @tryghost/bookshelf-search 0.1.32 - project: @tryghost/http-cache-utils 0.1.22 - project: @tryghost/mw-error-handler 1.0.10 - project: @tryghost/bookshelf-order 0.1.32 - project: @tryghost/api-framework 1.0.4 - project: @tryghost/database-info 0.3.32 - project: @tryghost/domain-events 1.0.5 - project: @tryghost/elasticsearch 3.0.26 - project: @tryghost/jest-snapshot 0.5.20 - project: @tryghost/pretty-stream 0.2.2 - project: @tryghost/express-test 0.15.2 - project: @tryghost/http-stream 0.1.39 - project: @tryghost/job-manager 1.0.6 - project: @tryghost/nodemailer 0.3.50 - project: @tryghost/pretty-cli 1.2.49 - project: @tryghost/root-utils 0.3.35 - project: @tryghost/validator 0.2.19 - project: @tryghost/mw-vhost 1.0.3 - project: @tryghost/security 1.0.3 - project: @tryghost/logging 2.5.2 - project: @tryghost/metrics 1.0.40 - project: @tryghost/promise 0.3.17 - project: @tryghost/request 1.0.14 - project: @tryghost/version 0.1.35 - project: @tryghost/config 0.2.29 - project: @tryghost/errors 1.3.10 - project: @tryghost/server 0.1.56 - project: @tryghost/debug 0.1.37 - project: @tryghost/tpl 0.1.37 - project: @tryghost/zip 1.1.51 --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 4c24b26bbcc..f825c818b34 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/api-framework", - "version": "1.0.3", + "version": "1.0.4", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", From ea60d6bb7df424c657d39214673e698ac40a451d Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Fri, 20 Feb 2026 15:13:45 -0600 Subject: [PATCH 076/118] Updated readmes and descriptions with basic info no ref --- ghost/api-framework/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ghost/api-framework/README.md b/ghost/api-framework/README.md index 9d31c3939cc..61ebbe7dac7 100644 --- a/ghost/api-framework/README.md +++ b/ghost/api-framework/README.md @@ -2,6 +2,10 @@ API framework used by Ghost +## Purpose + +Composable framework for Ghost API controllers, request framing, validation/serialization pipelines, and HTTP response helpers. + ## Usage ### Stages From 92303ca7c440b3433b91d83af293f20c28e6b0b0 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Fri, 20 Feb 2026 16:34:45 -0600 Subject: [PATCH 077/118] Refactored api-framework tests to use assert and updated coverage checks - Replaced 'should' assertions with 'assert' for consistency and improved readability in api-framework tests. - Updated the test script in package.json to enforce stricter coverage checks. - Added new test cases for module exports, frame handling, and header management. - Removed unused dependencies and improved overall test structure. --- ghost/api-framework/package.json | 3 +- .../api-framework/test/api-framework.test.js | 20 ++ ghost/api-framework/test/frame.test.js | 76 +++-- ghost/api-framework/test/headers.test.js | 29 +- ghost/api-framework/test/http.test.js | 124 +++++++- ghost/api-framework/test/pipeline.test.js | 275 ++++++++++++++---- .../test/serializers/handle.test.js | 9 +- .../test/serializers/input/all.test.js | 36 +-- ghost/api-framework/test/util/options.test.js | 19 +- .../test/validators/handle.test.js | 36 ++- .../test/validators/input/all.test.js | 150 ++++++++-- 11 files changed, 612 insertions(+), 165 deletions(-) create mode 100644 ghost/api-framework/test/api-framework.test.js diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index f825c818b34..64725a48d0a 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -13,7 +13,7 @@ "main": "index.js", "scripts": { "dev": "echo \"Implement me!\"", - "test:unit": "NODE_ENV=testing c8 --all --reporter text --reporter cobertura -- mocha --reporter dot './test/**/*.test.js'", + "test:unit": "NODE_ENV=testing c8 --all --reporter text --reporter cobertura --check-coverage --100 -- mocha --reporter dot './test/**/*.test.js'", "test": "yarn test:unit", "lint:code": "eslint *.js lib/ --ext .js --cache", "lint": "yarn lint:code && yarn lint:test", @@ -26,7 +26,6 @@ "devDependencies": { "c8": "8.0.1", "mocha": "11.7.5", - "should": "13.2.3", "sinon": "21.0.1" }, "dependencies": { diff --git a/ghost/api-framework/test/api-framework.test.js b/ghost/api-framework/test/api-framework.test.js new file mode 100644 index 00000000000..8c7a9c908cd --- /dev/null +++ b/ghost/api-framework/test/api-framework.test.js @@ -0,0 +1,20 @@ +const assert = require('node:assert/strict'); + +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); + assert.ok(apiFramework.Frame); + assert.ok(apiFramework.pipeline); + assert.ok(apiFramework.validators); + assert.ok(apiFramework.serializers); + assert.ok(apiFramework.utils); + }); + + it('exposes serializer output module', function () { + const serializers = require('../lib/serializers'); + assert.deepEqual(serializers.output, {}); + }); +}); diff --git a/ghost/api-framework/test/frame.test.js b/ghost/api-framework/test/frame.test.js index c53eb2dc1c8..8a42a4c0b6d 100644 --- a/ghost/api-framework/test/frame.test.js +++ b/ghost/api-framework/test/frame.test.js @@ -1,10 +1,10 @@ -const should = require('should'); +const assert = require('node:assert/strict'); const shared = require('../'); describe('Frame', function () { it('constructor', function () { const frame = new shared.Frame(); - Object.keys(frame).should.eql([ + assert.deepEqual(Object.keys(frame), [ 'original', 'options', 'data', @@ -31,13 +31,13 @@ describe('Frame', function () { frame.configure({}); - should.exist(frame.options.context.user); - should.not.exist(frame.options.include); - should.not.exist(frame.options.filter); - should.not.exist(frame.options.id); - should.not.exist(frame.options.soup); + assert.ok(frame.options.context.user); + assert.equal(frame.options.include, undefined); + assert.equal(frame.options.filter, undefined); + assert.equal(frame.options.id, undefined); + assert.equal(frame.options.soup, undefined); - should.exist(frame.data.posts); + assert.ok(frame.data.posts); }); it('transform with query', function () { @@ -54,13 +54,13 @@ describe('Frame', function () { options: ['include', 'filter', 'id'] }); - should.exist(frame.options.context.user); - should.exist(frame.options.include); - should.exist(frame.options.filter); - should.exist(frame.options.id); - should.not.exist(frame.options.soup); + assert.ok(frame.options.context.user); + assert.ok(frame.options.include); + assert.ok(frame.options.filter); + assert.ok(frame.options.id); + assert.equal(frame.options.soup, undefined); - should.exist(frame.data.posts); + assert.ok(frame.data.posts); }); it('transform', function () { @@ -77,8 +77,8 @@ describe('Frame', function () { options: ['include', 'filter', 'slug'] }); - should.exist(frame.options.context.user); - should.exist(frame.options.slug); + assert.ok(frame.options.context.user); + assert.ok(frame.options.slug); }); it('transform with data', function () { @@ -96,9 +96,47 @@ describe('Frame', function () { data: ['id'] }); - should.exist(frame.options.context.user); - should.not.exist(frame.options.id); - should.exist(frame.data.id); + assert.ok(frame.options.context.user); + assert.equal(frame.options.id, undefined); + assert.ok(frame.data.id); + }); + + it('supports options/data selectors as functions', function () { + const original = { + context: {user: 'id'}, + query: {include: 'tags'}, + params: {slug: 'abc'}, + options: {id: 'id'} + }; + + const frame = new shared.Frame(original); + + frame.configure({ + options() { + return ['include', 'slug']; + }, + data() { + 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'); + }); + }); + + describe('headers', function () { + it('sets and returns copied headers', function () { + const frame = new shared.Frame(); + frame.setHeader('X-Test', '1'); + + const headers = frame.getHeaders(); + assert.deepEqual(headers, {'X-Test': '1'}); + + headers['X-Test'] = '2'; + assert.deepEqual(frame.getHeaders(), {'X-Test': '1'}); }); }); }); diff --git a/ghost/api-framework/test/headers.test.js b/ghost/api-framework/test/headers.test.js index 941ba9160b3..250a53c2cb0 100644 --- a/ghost/api-framework/test/headers.test.js +++ b/ghost/api-framework/test/headers.test.js @@ -1,10 +1,11 @@ +const assert = require('node:assert/strict'); const shared = require('../'); const Frame = require('../lib/Frame'); describe('Headers', function () { it('empty headers config', function () { return shared.headers.get({}, {}, new Frame()).then((result) => { - result.should.eql({}); + assert.deepEqual(result, {}); }); }); @@ -12,7 +13,7 @@ describe('Headers', function () { it('json', function () { return shared.headers.get({}, {disposition: {type: 'json', value: 'value'}}, new Frame()) .then((result) => { - result.should.eql({ + assert.deepEqual(result, { 'Content-Disposition': 'Attachment; filename="value"', 'Content-Type': 'application/json', 'Content-Length': 2 @@ -23,7 +24,7 @@ describe('Headers', function () { it('csv', function () { return shared.headers.get({}, {disposition: {type: 'csv', value: 'my.csv'}}, new Frame()) .then((result) => { - result.should.eql({ + assert.deepEqual(result, { 'Content-Disposition': 'Attachment; filename="my.csv"', 'Content-Type': 'text/csv' }); @@ -41,7 +42,7 @@ describe('Headers', function () { } } }, new Frame()); - result.should.eql({ + assert.deepEqual(result, { 'Content-Disposition': 'Attachment; filename="awesome-data-2022-08-01.csv"', 'Content-Type': 'text/csv' }); @@ -49,7 +50,7 @@ describe('Headers', function () { it('file', async function () { const result = await shared.headers.get({}, {disposition: {type: 'file', value: 'my.txt'}}, new Frame()); - result.should.eql({ + assert.deepEqual(result, { 'Content-Disposition': 'Attachment; filename="my.txt"' }); }); @@ -65,7 +66,7 @@ describe('Headers', function () { } } }, new Frame()); - result.should.eql({ + assert.deepEqual(result, { 'Content-Disposition': 'Attachment; filename="awesome-data-2022-08-01.txt"' }); }); @@ -73,7 +74,7 @@ describe('Headers', function () { it('yaml', function () { return shared.headers.get('yaml file', {disposition: {type: 'yaml', value: 'my.yaml'}}, new Frame()) .then((result) => { - result.should.eql({ + assert.deepEqual(result, { 'Content-Disposition': 'Attachment; filename="my.yaml"', 'Content-Type': 'application/yaml', 'Content-Length': 11 @@ -86,7 +87,7 @@ describe('Headers', function () { it('default', function () { return shared.headers.get({}, {cacheInvalidate: true}, new Frame()) .then((result) => { - result.should.eql({ + assert.deepEqual(result, { 'X-Cache-Invalidate': '/*' }); }); @@ -95,7 +96,7 @@ describe('Headers', function () { it('custom value', function () { return shared.headers.get({}, {cacheInvalidate: {value: 'value'}}, new Frame()) .then((result) => { - result.should.eql({ + assert.deepEqual(result, { 'X-Cache-Invalidate': 'value' }); }); @@ -123,7 +124,7 @@ describe('Headers', function () { return shared.headers.get(apiResult, apiConfigHeaders, frame) .then((result) => { - result.should.eql({ + 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/' }); @@ -158,7 +159,7 @@ describe('Headers', function () { return shared.headers.get(apiResult, apiConfigHeaders, frame) .then((result) => { - result.should.eql({ + assert.deepEqual(result, { Location: resolvedLocationUrl }); }); @@ -185,7 +186,7 @@ describe('Headers', function () { }; const result = await shared.headers.get(apiResult, apiConfigHeaders, frame); - result.should.eql({ + 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/' }); @@ -211,7 +212,7 @@ describe('Headers', function () { return shared.headers.get(apiResult, apiConfigHeaders, frame) .then((result) => { - result.should.eql({ + 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/' }); @@ -234,7 +235,7 @@ describe('Headers', function () { return shared.headers.get(apiResult, apiConfigHeaders, frame) .then((result) => { - result.should.eql({}); + assert.deepEqual(result, {}); }); }); }); diff --git a/ghost/api-framework/test/http.test.js b/ghost/api-framework/test/http.test.js index ad7a0381612..3e41407cea7 100644 --- a/ghost/api-framework/test/http.test.js +++ b/ghost/api-framework/test/http.test.js @@ -1,4 +1,4 @@ -const should = require('should'); +const assert = require('node:assert/strict'); const sinon = require('sinon'); const shared = require('../'); @@ -18,6 +18,9 @@ describe('HTTP', function () { req.vhost = { host: 'example.com' }; + 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(); @@ -38,7 +41,7 @@ describe('HTTP', function () { const apiImpl = sinon.stub().resolves(); shared.http(apiImpl)(req, res, next); - Object.keys(apiImpl.args[0][0]).should.eql([ + assert.deepEqual(Object.keys(apiImpl.args[0][0]), [ 'original', 'options', 'data', @@ -51,8 +54,8 @@ describe('HTTP', function () { 'response' ]); - apiImpl.args[0][0].data.should.eql({a: 'a'}); - apiImpl.args[0][0].options.should.eql({ + assert.deepEqual(apiImpl.args[0][0].data, {a: 'a'}); + assert.deepEqual(apiImpl.args[0][0].options, { context: { api_key: null, integration: null, @@ -64,11 +67,11 @@ describe('HTTP', function () { it('api response is fn', function (done) { const response = sinon.stub().callsFake(function (_req, _res, _next) { - should.exist(_req); - should.exist(_res); - should.exist(_next); - apiImpl.calledOnce.should.be.true(); - _res.json.called.should.be.false(); + assert.ok(_req); + assert.ok(_res); + assert.ok(_next); + assert.equal(apiImpl.calledOnce, true); + assert.equal(_res.json.called, false); done(); }); @@ -82,9 +85,106 @@ describe('HTTP', function () { next.callsFake(done); res.json.callsFake(function () { - shared.headers.get.calledOnce.should.be.true(); - res.status.calledOnce.should.be.true(); - res.send.called.should.be.false(); + assert.equal(shared.headers.get.calledOnce, true); + assert.equal(res.status.calledOnce, true); + assert.equal(res.send.called, false); + done(); + }); + + shared.http(apiImpl)(req, res, next); + }); + + it('handles api key, user and plain text response', function (done) { + req.vhost = null; + req.user = {id: 'user-id'}; + req.api_key = { + get(key) { + return { + id: 'api-key-id', + type: 'admin', + integration_id: 'integration-id' + }[key]; + } + }; + + const apiImpl = sinon.stub().resolves('plain body'); + apiImpl.response = {format: 'plain'}; + apiImpl.statusCode = 201; + + res.send.callsFake(() => { + assert.equal(res.status.calledOnceWithExactly(201), true); + assert.equal(res.headers.constructor, Object); + 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'); + done(); + }); + + shared.http(apiImpl)(req, res, next); + }); + + it('supports async response format and statusCode function', function (done) { + const apiImpl = sinon.stub().resolves({ok: true}); + apiImpl.statusCode = sinon.stub().returns(204); + apiImpl.response = { + format() { + return Promise.resolve('plain'); + } + }; + + res.send.callsFake(() => { + assert.equal(apiImpl.statusCode.calledOnce, true); + assert.equal(res.status.calledOnceWithExactly(204), true); + done(); + }); + + shared.http(apiImpl)(req, res, next); + }); + + it('supports sync response format function', function (done) { + const apiImpl = sinon.stub().resolves('plain body'); + apiImpl.response = { + format() { + return 'plain'; + } + }; + + res.send.callsFake(() => { + assert.equal(res.send.calledOnce, true); + assert.equal(res.json.called, false); + done(); + }); + + shared.http(apiImpl)(req, res, next); + }); + + it('passes errors to next with frame options', function (done) { + 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 + }); + done(); + }); + + shared.http(apiImpl)(req, res, next); + }); + + it('uses req.url pathname when originalUrl is missing', function (done) { + req.originalUrl = undefined; + 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/'); done(); }); diff --git a/ghost/api-framework/test/pipeline.test.js b/ghost/api-framework/test/pipeline.test.js index 3e57081eaad..b0e7e88e84a 100644 --- a/ghost/api-framework/test/pipeline.test.js +++ b/ghost/api-framework/test/pipeline.test.js @@ -1,5 +1,5 @@ const errors = require('@tryghost/errors'); -const should = require('should'); +const assert = require('node:assert/strict'); const sinon = require('sinon'); const shared = require('../'); @@ -25,10 +25,10 @@ describe('Pipeline', function () { return shared.pipeline.STAGES.validation.input(apiUtils, apiConfig, apiImpl, frame) .then((response) => { - response.should.eql('response'); + assert.equal(response, 'response'); - apiImpl.validation.calledOnce.should.be.true(); - shared.validators.handle.input.called.should.be.false(); + assert.equal(apiImpl.validation.calledOnce, true); + assert.equal(shared.validators.handle.input.called, false); }); }); @@ -59,8 +59,8 @@ describe('Pipeline', function () { return shared.pipeline.STAGES.validation.input(apiUtils, apiConfig, apiImpl, frame) .then(() => { - shared.validators.handle.input.calledOnce.should.be.true(); - shared.validators.handle.input.calledWith( + assert.equal(shared.validators.handle.input.calledOnce, true); + assert.equal(shared.validators.handle.input.calledWith( { docName: 'posts', options: { @@ -74,12 +74,48 @@ describe('Pipeline', function () { }, { options: {} - }).should.be.true(); + }), true); }); }); }); }); + 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 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' + }); + }); + }); + + 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 apiImpl = {}; + const frame = {}; + const response = [{id: '1'}]; + + 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); + }); + }); + }); + describe('permissions', function () { let apiUtils; @@ -99,8 +135,8 @@ describe('Pipeline', function () { return shared.pipeline.STAGES.permissions(apiUtils, apiConfig, apiImpl, frame) .then(Promise.reject) .catch((err) => { - (err instanceof errors.IncorrectUsageError).should.be.true(); - apiUtils.permissions.handle.called.should.be.false(); + assert.equal(err instanceof errors.IncorrectUsageError, true); + assert.equal(apiUtils.permissions.handle.called, false); }); }); @@ -113,9 +149,9 @@ describe('Pipeline', function () { return shared.pipeline.STAGES.permissions(apiUtils, apiConfig, apiImpl, frame) .then((response) => { - response.should.eql('lol'); - apiImpl.permissions.calledOnce.should.be.true(); - apiUtils.permissions.handle.called.should.be.false(); + assert.equal(response, 'lol'); + assert.equal(apiImpl.permissions.calledOnce, true); + assert.equal(apiUtils.permissions.handle.called, false); }); }); @@ -128,7 +164,7 @@ describe('Pipeline', function () { return shared.pipeline.STAGES.permissions(apiUtils, apiConfig, apiImpl, frame) .then(() => { - apiUtils.permissions.handle.called.should.be.false(); + assert.equal(apiUtils.permissions.handle.called, false); }); }); @@ -141,7 +177,7 @@ describe('Pipeline', function () { return shared.pipeline.STAGES.permissions(apiUtils, apiConfig, apiImpl, frame) .then(() => { - apiUtils.permissions.handle.calledOnce.should.be.true(); + assert.equal(apiUtils.permissions.handle.calledOnce, true); }); }); @@ -160,15 +196,52 @@ describe('Pipeline', function () { return shared.pipeline.STAGES.permissions(apiUtils, apiConfig, apiImpl, frame) .then(() => { - apiUtils.permissions.handle.calledOnce.should.be.true(); - apiUtils.permissions.handle.calledWith( + assert.equal(apiUtils.permissions.handle.calledOnce, true); + assert.equal(apiUtils.permissions.handle.calledWith( { docName: 'posts', unsafeAttrs: ['test'] }, { options: {} - }).should.be.true(); + }), true); + }); + }); + + it('runs permission before hook', function () { + const before = sinon.stub().resolves(); + const apiConfig = {}; + const apiImpl = { + permissions: { + before + } + }; + const frame = {}; + + 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 () { + return shared.pipeline.STAGES.query({}, {}, {}, {}) + .then(Promise.reject) + .catch((err) => { + assert.equal(err instanceof errors.IncorrectUsageError, true); + }); + }); + + 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); }); }); }); @@ -192,12 +265,12 @@ describe('Pipeline', function () { const apiUtils = {}; const result = shared.pipeline(apiController, apiUtils); - result.should.be.an.Object(); + assert.equal(typeof result, 'object'); - should.exist(result.add); - should.exist(result.browse); - result.add.should.be.a.Function(); - result.browse.should.be.a.Function(); + assert.ok(result.add); + assert.ok(result.browse); + assert.equal(typeof result.add, 'function'); + assert.equal(typeof result.browse, 'function'); }); it('call api controller fn', function () { @@ -218,13 +291,70 @@ describe('Pipeline', function () { return result.add() .then((response) => { - response.should.eql('response'); + assert.equal(response, 'response'); - shared.pipeline.STAGES.validation.input.calledOnce.should.be.true(); - shared.pipeline.STAGES.serialisation.input.calledOnce.should.be.true(); - shared.pipeline.STAGES.permissions.calledOnce.should.be.true(); - shared.pipeline.STAGES.query.calledOnce.should.be.true(); - shared.pipeline.STAGES.serialisation.output.calledOnce.should.be.true(); + 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 () { + const apiController = { + docName: 'posts', + add: { + headers: {}, + permissions: true, + query: sinon.stub().resolves('response') + } + }; + + const apiUtils = {}; + const result = shared.pipeline(apiController, apiUtils); + + 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}}) + .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}); + }); + }); + + it('supports single undefined argument by defaulting options', function () { + const apiController = { + docName: 'posts', + add: { + headers: {}, + permissions: true, + query: sinon.stub().resolves('response') + } + }; + + const apiUtils = {}; + const result = shared.pipeline(apiController, apiUtils); + + 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(undefined) + .then(() => { + const frame = shared.pipeline.STAGES.validation.input.args[0][3]; + assert.deepEqual(frame.options.context, {}); }); }); @@ -240,15 +370,62 @@ describe('Pipeline', function () { return result.add() .then((response) => { - response.should.eql('response'); + assert.equal(response, 'response'); - shared.pipeline.STAGES.validation.input.called.should.be.false(); - shared.pipeline.STAGES.serialisation.input.called.should.be.false(); - shared.pipeline.STAGES.permissions.called.should.be.false(); - shared.pipeline.STAGES.query.called.should.be.false(); - shared.pipeline.STAGES.serialisation.output.called.should.be.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 () { + const apiController = { + browse: { + headers: {}, + permissions: true, + generateCacheKeyData: sinon.stub().resolves({custom: 'key'}), + query: sinon.stub().resolves('response') + } + }; + + const apiUtils = {}; + 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; + }); + + 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(frame.docName, undefined); + assert.equal(frame.method, 'browse'); + }); + + it('returns cached controller wrapper for same controller object', function () { + const apiController = { + docName: 'posts', + browse: { + headers: {}, + permissions: true, + query: sinon.stub().resolves('response') + } + }; + + const first = shared.pipeline(apiController, {}); + const second = shared.pipeline(apiController, {}); + + assert.equal(first, second); + }); }); describe('caching', function () { @@ -283,18 +460,18 @@ describe('Pipeline', function () { const response = await result.browse(); - response.should.eql('response'); + assert.equal(response, 'response'); // request went through all stages - shared.pipeline.STAGES.validation.input.calledOnce.should.be.true(); - shared.pipeline.STAGES.serialisation.input.calledOnce.should.be.true(); - shared.pipeline.STAGES.permissions.calledOnce.should.be.true(); - shared.pipeline.STAGES.query.calledOnce.should.be.true(); - shared.pipeline.STAGES.serialisation.output.calledOnce.should.be.true(); + 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); // cache was set - apiController.browse.cache.set.calledOnce.should.be.true(); - apiController.browse.cache.set.args[0][1].should.equal('response'); + assert.equal(apiController.browse.cache.set.calledOnce, true); + assert.equal(apiController.browse.cache.set.args[0][1], 'response'); }); it('should use cache if configured on endpoint level', async function () { @@ -320,17 +497,17 @@ describe('Pipeline', function () { const response = await result.browse(); - response.should.eql('CACHED RESPONSE'); + assert.equal(response, 'CACHED RESPONSE'); // request went through all stages - shared.pipeline.STAGES.validation.input.calledOnce.should.be.false(); - shared.pipeline.STAGES.serialisation.input.calledOnce.should.be.false(); - shared.pipeline.STAGES.permissions.calledOnce.should.be.false(); - shared.pipeline.STAGES.query.calledOnce.should.be.false(); - shared.pipeline.STAGES.serialisation.output.calledOnce.should.be.false(); + assert.equal(shared.pipeline.STAGES.validation.input.calledOnce, false); + assert.equal(shared.pipeline.STAGES.serialisation.input.calledOnce, false); + assert.equal(shared.pipeline.STAGES.permissions.calledOnce, false); + assert.equal(shared.pipeline.STAGES.query.calledOnce, false); + assert.equal(shared.pipeline.STAGES.serialisation.output.calledOnce, false); // cache not set - apiController.browse.cache.set.calledOnce.should.be.false(); + assert.equal(apiController.browse.cache.set.calledOnce, false); }); }); }); diff --git a/ghost/api-framework/test/serializers/handle.test.js b/ghost/api-framework/test/serializers/handle.test.js index 24612ac85c0..06f5f8d8e77 100644 --- a/ghost/api-framework/test/serializers/handle.test.js +++ b/ghost/api-framework/test/serializers/handle.test.js @@ -1,4 +1,5 @@ const errors = require('@tryghost/errors'); +const assert = require('node:assert/strict'); const sinon = require('sinon'); const shared = require('../../'); @@ -12,7 +13,7 @@ describe('serializers/handle', function () { return shared.serializers.handle.input() .then(Promise.reject) .catch((err) => { - (err instanceof errors.IncorrectUsageError).should.be.true(); + assert.equal(err instanceof errors.IncorrectUsageError, true); }); }); @@ -20,7 +21,7 @@ describe('serializers/handle', function () { return shared.serializers.handle.input({}) .then(Promise.reject) .catch((err) => { - (err instanceof errors.IncorrectUsageError).should.be.true(); + assert.equal(err instanceof errors.IncorrectUsageError, true); }); }); @@ -112,7 +113,7 @@ describe('serializers/handle', function () { return shared.serializers.handle.output([]) .then(Promise.reject) .catch((err) => { - (err instanceof errors.IncorrectUsageError).should.be.true(); + assert.equal(err instanceof errors.IncorrectUsageError, true); }); }); @@ -120,7 +121,7 @@ describe('serializers/handle', function () { return shared.serializers.handle.output([], {}) .then(Promise.reject) .catch((err) => { - (err instanceof errors.IncorrectUsageError).should.be.true(); + assert.equal(err instanceof errors.IncorrectUsageError, true); }); }); diff --git a/ghost/api-framework/test/serializers/input/all.test.js b/ghost/api-framework/test/serializers/input/all.test.js index 8148f280744..379848d9584 100644 --- a/ghost/api-framework/test/serializers/input/all.test.js +++ b/ghost/api-framework/test/serializers/input/all.test.js @@ -1,4 +1,4 @@ -const should = require('should'); +const assert = require('node:assert/strict'); const shared = require('../../../'); describe('serializers/input/all', function () { @@ -21,19 +21,19 @@ describe('serializers/input/all', function () { shared.serializers.input.all.all(apiConfig, frame); - should.exist(frame.original.include); - should.exist(frame.original.fields); - should.exist(frame.original.formats); + assert.ok(frame.original.include); + assert.ok(frame.original.fields); + assert.ok(frame.original.formats); - should.not.exist(frame.options.include); - should.not.exist(frame.options.fields); - should.exist(frame.options.formats); - should.exist(frame.options.columns); - should.exist(frame.options.withRelated); + assert.equal(frame.options.include, undefined); + assert.equal(frame.options.fields, undefined); + assert.ok(frame.options.formats); + assert.ok(frame.options.columns); + assert.ok(frame.options.withRelated); - frame.options.withRelated.should.eql(['tags']); - frame.options.columns.should.eql(['id','status','html']); - frame.options.formats.should.eql(['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 () { @@ -52,9 +52,9 @@ describe('serializers/input/all', function () { shared.serializers.input.all.all(apiConfig, frame); - should.exist(frame.options.transacting); - should.exist(frame.options.forUpdate); - should.exist(frame.options.context); + assert.ok(frame.options.transacting); + assert.ok(frame.options.forUpdate); + assert.ok(frame.options.context); }); it('no internal access', function () { @@ -72,9 +72,9 @@ describe('serializers/input/all', function () { shared.serializers.input.all.all(apiConfig, frame); - should.not.exist(frame.options.transacting); - should.not.exist(frame.options.forUpdate); - should.exist(frame.options.context); + assert.equal(frame.options.transacting, undefined); + assert.equal(frame.options.forUpdate, undefined); + assert.ok(frame.options.context); }); }); }); diff --git a/ghost/api-framework/test/util/options.test.js b/ghost/api-framework/test/util/options.test.js index ad926f8991c..2dbb4bcd75d 100644 --- a/ghost/api-framework/test/util/options.test.js +++ b/ghost/api-framework/test/util/options.test.js @@ -1,31 +1,30 @@ +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 () { - optionsUtil.trimAndLowerCase().should.eql(['']); + assert.deepEqual(optionsUtil.trimAndLowerCase(), ['']); }); it('returns single item array', function () { - optionsUtil.trimAndLowerCase('butter').should.eql(['butter']); + assert.deepEqual(optionsUtil.trimAndLowerCase('butter'), ['butter']); }); it('returns multiple items in array', function () { - optionsUtil.trimAndLowerCase('peanut, butter').should.eql(['peanut', 'butter']); + assert.deepEqual(optionsUtil.trimAndLowerCase('peanut, butter'), ['peanut', 'butter']); }); it('lowercases and trims items in the string', function () { - optionsUtil.trimAndLowerCase(' PeanUt, buTTer ').should.eql(['peanut', 'butter']); + assert.deepEqual(optionsUtil.trimAndLowerCase(' PeanUt, buTTer '), ['peanut', 'butter']); }); it('accepts parameters in form of an array', function () { - optionsUtil.trimAndLowerCase([' PeanUt', ' buTTer ']).should.eql(['peanut', 'butter']); + assert.deepEqual(optionsUtil.trimAndLowerCase([' PeanUt', ' buTTer ']), ['peanut', 'butter']); }); it('throws error for invalid object input', function () { - try { - optionsUtil.trimAndLowerCase({name: 'peanut'}); - } catch (err) { - err.message.should.eql('Params must be a string or array'); - } + assert.throws(() => optionsUtil.trimAndLowerCase({name: 'peanut'}), { + message: 'Params must be a string or array' + }); }); }); diff --git a/ghost/api-framework/test/validators/handle.test.js b/ghost/api-framework/test/validators/handle.test.js index da8bf5dad84..86421f12acc 100644 --- a/ghost/api-framework/test/validators/handle.test.js +++ b/ghost/api-framework/test/validators/handle.test.js @@ -1,4 +1,5 @@ const errors = require('@tryghost/errors'); +const assert = require('node:assert/strict'); const sinon = require('sinon'); const shared = require('../../'); @@ -12,7 +13,7 @@ describe('validators/handle', function () { return shared.validators.handle.input() .then(Promise.reject) .catch((err) => { - (err instanceof errors.IncorrectUsageError).should.be.true(); + assert.equal(err instanceof errors.IncorrectUsageError, true); }); }); @@ -20,7 +21,15 @@ describe('validators/handle', function () { return shared.validators.handle.input({}) .then(Promise.reject) .catch((err) => { - (err instanceof errors.IncorrectUsageError).should.be.true(); + assert.equal(err instanceof errors.IncorrectUsageError, true); + }); + }); + + 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); }); }); @@ -48,11 +57,24 @@ describe('validators/handle', function () { return shared.validators.handle.input({docName: 'posts', method: 'add'}, apiValidators, {context: {}}) .then(() => { - getStub.calledOnce.should.be.true(); - addStub.calledOnce.should.be.true(); - apiValidators.all.add.calledOnce.should.be.true(); - apiValidators.posts.add.calledOnce.should.be.true(); - apiValidators.users.add.called.should.be.false(); + assert.equal(getStub.calledOnce, true); + assert.equal(addStub.calledOnce, true); + assert.equal(apiValidators.all.add.calledOnce, true); + assert.equal(apiValidators.posts.add.calledOnce, true); + assert.equal(apiValidators.users.add.called, false); + }); + }); + + it('calls docName all validator when provided', function () { + const apiValidators = { + posts: { + all: sinon.stub().resolves() + } + }; + + return shared.validators.handle.input({docName: 'posts', method: 'browse'}, apiValidators, {}) + .then(() => { + assert.equal(apiValidators.posts.all.calledOnce, true); }); }); }); diff --git a/ghost/api-framework/test/validators/input/all.test.js b/ghost/api-framework/test/validators/input/all.test.js index c35a9da3d78..66febed0e16 100644 --- a/ghost/api-framework/test/validators/input/all.test.js +++ b/ghost/api-framework/test/validators/input/all.test.js @@ -1,5 +1,5 @@ const errors = require('@tryghost/errors'); -const should = require('should'); +const assert = require('node:assert/strict'); const sinon = require('sinon'); const shared = require('../../../'); @@ -30,10 +30,10 @@ describe('validators/input/all', function () { return shared.validators.input.all.all(apiConfig, frame) .then(() => { - should.exist(frame.options.page); - should.exist(frame.options.slug); - should.exist(frame.options.include); - should.exist(frame.options.context); + assert.ok(frame.options.page); + assert.ok(frame.options.slug); + assert.ok(frame.options.include); + assert.ok(frame.options.context); }); }); @@ -56,7 +56,7 @@ describe('validators/input/all', function () { .then(() => { throw new Error('Should not resolve'); }, (err) => { - should.exist(err); + assert.ok(err); }); }); @@ -98,10 +98,10 @@ describe('validators/input/all', function () { return shared.validators.input.all.all(apiConfig, frame) .then(() => { - should.exist(frame.options.page); - should.exist(frame.options.slug); - should.exist(frame.options.include); - should.exist(frame.options.context); + assert.ok(frame.options.page); + assert.ok(frame.options.slug); + assert.ok(frame.options.include); + assert.ok(frame.options.context); }); }); @@ -123,10 +123,10 @@ describe('validators/input/all', function () { return shared.validators.input.all.all(apiConfig, frame) .then(() => { - should.exist(frame.options.page); - should.exist(frame.options.slug); - should.exist(frame.options.include); - should.exist(frame.options.context); + assert.ok(frame.options.page); + assert.ok(frame.options.slug); + assert.ok(frame.options.include); + assert.ok(frame.options.context); }); }); @@ -149,7 +149,7 @@ describe('validators/input/all', function () { return shared.validators.input.all.all(apiConfig, frame) .then(Promise.reject.bind(Promise)) .catch((err) => { - should.not.exist(err); + assert.equal(err, undefined); }); }); @@ -170,7 +170,7 @@ describe('validators/input/all', function () { return shared.validators.input.all.all(apiConfig, frame) .then(Promise.reject.bind(Promise)) .catch((err) => { - should.not.exist(err); + assert.equal(err, undefined); }); }); @@ -192,7 +192,7 @@ describe('validators/input/all', function () { return shared.validators.input.all.all(apiConfig, frame) .then(Promise.reject) .catch((err) => { - should.exist(err); + assert.ok(err); }); }); @@ -209,7 +209,29 @@ describe('validators/input/all', function () { return shared.validators.input.all.all(apiConfig, frame) .then(Promise.reject) .catch((err) => { - should.exist(err); + assert.ok(err); + }); + }); + + it('fails on invalid allowed values for non-include fields', function () { + const frame = { + options: { + context: {}, + formats: 'mobiledoc' + } + }; + + const apiConfig = { + options: { + formats: ['html'] + } + }; + + 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'); }); }); }); @@ -228,8 +250,8 @@ describe('validators/input/all', function () { const apiConfig = {}; shared.validators.input.all.browse(apiConfig, frame); - should.exist(frame.options.context); - should.exist(frame.data.status); + assert.ok(frame.options.context); + assert.ok(frame.data.status); }); it('fails', function () { @@ -247,7 +269,7 @@ describe('validators/input/all', function () { return shared.validators.input.all.browse(apiConfig, frame) .then(Promise.reject) .catch((err) => { - should.exist(err); + assert.ok(err); }); }); }); @@ -265,7 +287,7 @@ describe('validators/input/all', function () { const apiConfig = {}; shared.validators.input.all.read(apiConfig, frame); - shared.validators.input.all.browse.calledOnce.should.be.true(); + assert.equal(shared.validators.input.all.browse.calledOnce, true); }); }); @@ -282,7 +304,7 @@ describe('validators/input/all', function () { return shared.validators.input.all.add(apiConfig, frame) .then(Promise.reject) .catch((err) => { - should.exist(err); + assert.ok(err); }); }); @@ -300,7 +322,7 @@ describe('validators/input/all', function () { return shared.validators.input.all.add(apiConfig, frame) .then(Promise.reject) .catch((err) => { - should.exist(err); + assert.ok(err); }); }); @@ -325,8 +347,8 @@ describe('validators/input/all', function () { return shared.validators.input.all.add(apiConfig, frame) .then(Promise.reject) .catch((err) => { - should.exist(err); - err.message.should.eql('Validation (FieldIsRequired) failed for ["b"]'); + assert.ok(err); + assert.equal(err.message, 'Validation (FieldIsRequired) failed for ["b"]'); }); }); @@ -352,8 +374,8 @@ describe('validators/input/all', function () { return shared.validators.input.all.add(apiConfig, frame) .then(Promise.reject) .catch((err) => { - should.exist(err); - err.message.should.eql('Validation (FieldIsInvalid) failed for ["b"]'); + assert.ok(err); + assert.equal(err.message, 'Validation (FieldIsInvalid) failed for ["b"]'); }); }); @@ -371,7 +393,7 @@ describe('validators/input/all', function () { }; const result = shared.validators.input.all.add(apiConfig, frame); - (result instanceof Promise).should.not.be.true(); + assert.equal(result instanceof Promise, false); }); }); @@ -397,8 +419,76 @@ describe('validators/input/all', function () { return shared.validators.input.all.edit(apiConfig, frame) .then(Promise.reject) .catch((err) => { - (err instanceof errors.BadRequestError).should.be.true(); + 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({}, {}) + .then(Promise.reject) + .catch((err) => { + 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'}] + } + }) + .then(Promise.reject) + .catch((err) => { + assert.equal(err instanceof errors.BadRequestError, true); + 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'}] + } + }); + assert.equal(result, undefined); + }); + }); + + 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'); + }); + + 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'); + }); + + 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'); + }); + + 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'); + }); }); }); From d72882f34d94492c15e6b61a06db36b0456836d6 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Fri, 20 Feb 2026 17:04:32 -0600 Subject: [PATCH 078/118] Published new versions - project: @tryghost/bookshelf-transaction-events 0.2.22 - project: @tryghost/bookshelf-include-count 0.3.21 - project: @tryghost/bookshelf-custom-query 0.1.33 - project: @tryghost/webhook-mock-receiver 0.2.20 - project: @tryghost/bookshelf-eager-load 0.1.37 - project: @tryghost/bookshelf-pagination 0.1.55 - project: @tryghost/bookshelf-collision 0.1.51 - project: @tryghost/bookshelf-has-posts 0.1.38 - project: @tryghost/email-mock-receiver 0.3.14 - project: @tryghost/prometheus-metrics 1.0.6 - project: @tryghost/bookshelf-plugins 0.6.31 - project: @tryghost/bookshelf-filter 0.5.26 - project: @tryghost/bookshelf-search 0.1.33 - project: @tryghost/http-cache-utils 0.1.23 - project: @tryghost/mw-error-handler 1.0.11 - project: @tryghost/bookshelf-order 0.1.33 - project: @tryghost/api-framework 1.0.5 - project: @tryghost/database-info 0.3.33 - project: @tryghost/domain-events 1.0.6 - project: @tryghost/elasticsearch 3.0.27 - project: @tryghost/jest-snapshot 0.5.21 - project: @tryghost/pretty-stream 0.2.3 - project: @tryghost/express-test 0.15.3 - project: @tryghost/http-stream 0.1.40 - project: @tryghost/job-manager 1.0.7 - project: @tryghost/nodemailer 0.3.51 - project: @tryghost/pretty-cli 1.2.50 - project: @tryghost/root-utils 0.3.36 - project: @tryghost/validator 0.2.20 - project: @tryghost/mw-vhost 1.0.4 - project: @tryghost/security 1.0.4 - project: @tryghost/logging 2.5.3 - project: @tryghost/metrics 1.0.41 - project: @tryghost/promise 0.3.18 - project: @tryghost/request 1.0.15 - project: @tryghost/version 0.1.36 - project: @tryghost/config 0.2.30 - project: @tryghost/errors 1.3.11 - project: @tryghost/server 0.1.57 - project: @tryghost/debug 0.1.38 - project: @tryghost/tpl 0.1.38 - project: @tryghost/zip 1.1.52 --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 64725a48d0a..1743c756ab9 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/api-framework", - "version": "1.0.4", + "version": "1.0.5", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", From 07ca3ac1ebeae238f242cf9381b832d431511320 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Fri, 20 Feb 2026 17:38:16 -0600 Subject: [PATCH 079/118] Published new versions - project: @tryghost/bookshelf-transaction-events 0.2.23 - project: @tryghost/bookshelf-include-count 0.3.22 - project: @tryghost/bookshelf-custom-query 0.1.34 - project: @tryghost/webhook-mock-receiver 0.2.21 - project: @tryghost/bookshelf-eager-load 0.1.38 - project: @tryghost/bookshelf-pagination 0.1.56 - project: @tryghost/bookshelf-collision 0.1.52 - project: @tryghost/bookshelf-has-posts 0.1.39 - project: @tryghost/email-mock-receiver 0.3.15 - project: @tryghost/prometheus-metrics 1.0.7 - project: @tryghost/bookshelf-plugins 0.6.32 - project: @tryghost/bookshelf-filter 0.5.27 - project: @tryghost/bookshelf-search 0.1.34 - project: @tryghost/http-cache-utils 0.1.24 - project: @tryghost/mw-error-handler 1.0.12 - project: @tryghost/bookshelf-order 0.1.34 - project: @tryghost/api-framework 1.0.6 - project: @tryghost/database-info 0.3.34 - project: @tryghost/domain-events 1.0.7 - project: @tryghost/elasticsearch 3.0.28 - project: @tryghost/jest-snapshot 0.5.22 - project: @tryghost/pretty-stream 0.2.4 - project: @tryghost/express-test 0.15.4 - project: @tryghost/http-stream 0.1.41 - project: @tryghost/job-manager 1.0.8 - project: @tryghost/nodemailer 0.3.52 - project: @tryghost/pretty-cli 1.2.51 - project: @tryghost/root-utils 0.3.37 - project: @tryghost/validator 0.2.21 - project: @tryghost/mw-vhost 1.0.5 - project: @tryghost/security 1.0.5 - project: @tryghost/logging 2.5.4 - project: @tryghost/metrics 1.0.42 - project: @tryghost/promise 0.3.19 - project: @tryghost/request 1.0.16 - project: @tryghost/version 0.1.37 - project: @tryghost/config 0.2.31 - project: @tryghost/errors 1.3.12 - project: @tryghost/server 0.1.58 - project: @tryghost/debug 0.1.39 - project: @tryghost/tpl 0.1.39 - project: @tryghost/zip 1.1.53 --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 1743c756ab9..cbccb14fdce 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/api-framework", - "version": "1.0.5", + "version": "1.0.6", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", From 4834fa558683221c6afdd8162f6338b06b7dd019 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Fri, 20 Feb 2026 17:53:34 -0600 Subject: [PATCH 080/118] Published new versions - project: @tryghost/bookshelf-transaction-events 0.2.24 - project: @tryghost/bookshelf-include-count 0.3.23 - project: @tryghost/bookshelf-custom-query 0.1.35 - project: @tryghost/webhook-mock-receiver 0.2.22 - project: @tryghost/bookshelf-eager-load 0.1.39 - project: @tryghost/bookshelf-pagination 0.1.57 - project: @tryghost/bookshelf-collision 0.1.53 - project: @tryghost/bookshelf-has-posts 0.1.40 - project: @tryghost/email-mock-receiver 0.3.16 - project: @tryghost/prometheus-metrics 1.0.8 - project: @tryghost/bookshelf-plugins 0.6.33 - project: @tryghost/bookshelf-filter 0.5.28 - project: @tryghost/bookshelf-search 0.1.35 - project: @tryghost/http-cache-utils 0.1.25 - project: @tryghost/mw-error-handler 1.0.13 - project: @tryghost/bookshelf-order 0.1.35 - project: @tryghost/api-framework 1.0.7 - project: @tryghost/database-info 0.3.35 - project: @tryghost/domain-events 1.0.8 - project: @tryghost/elasticsearch 3.0.29 - project: @tryghost/jest-snapshot 0.5.23 - project: @tryghost/pretty-stream 0.2.5 - project: @tryghost/express-test 0.15.5 - project: @tryghost/http-stream 0.1.42 - project: @tryghost/job-manager 1.0.9 - project: @tryghost/nodemailer 0.3.53 - project: @tryghost/pretty-cli 1.2.52 - project: @tryghost/root-utils 0.3.38 - project: @tryghost/validator 0.2.22 - project: @tryghost/mw-vhost 1.0.6 - project: @tryghost/security 1.0.6 - project: @tryghost/logging 2.5.5 - project: @tryghost/metrics 1.0.43 - project: @tryghost/promise 0.3.20 - project: @tryghost/request 1.0.17 - project: @tryghost/version 0.1.38 - project: @tryghost/config 0.2.32 - project: @tryghost/errors 1.3.13 - project: @tryghost/server 0.1.59 - project: @tryghost/debug 0.1.40 - project: @tryghost/tpl 0.1.40 - project: @tryghost/zip 1.1.54 --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index cbccb14fdce..d92d4b6fd1b 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/api-framework", - "version": "1.0.6", + "version": "1.0.7", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", From 0d94980ec41ff0c040f5c8436cdd96d86921d10e Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Fri, 20 Feb 2026 19:35:56 -0600 Subject: [PATCH 081/118] Published new versions - project: @tryghost/bookshelf-transaction-events 1.0.0 - project: @tryghost/bookshelf-include-count 1.0.0 - project: @tryghost/bookshelf-custom-query 1.0.0 - project: @tryghost/webhook-mock-receiver 1.0.0 - project: @tryghost/bookshelf-eager-load 1.0.0 - project: @tryghost/bookshelf-pagination 1.0.0 - project: @tryghost/bookshelf-collision 1.0.0 - project: @tryghost/bookshelf-has-posts 1.0.0 - project: @tryghost/email-mock-receiver 1.0.0 - project: @tryghost/prometheus-metrics 2.0.0 - project: @tryghost/bookshelf-plugins 1.0.0 - project: @tryghost/bookshelf-filter 1.0.0 - project: @tryghost/bookshelf-search 1.0.0 - project: @tryghost/http-cache-utils 1.0.0 - project: @tryghost/mw-error-handler 2.0.0 - project: @tryghost/bookshelf-order 1.0.0 - project: @tryghost/api-framework 2.0.0 - project: @tryghost/database-info 1.0.0 - project: @tryghost/domain-events 2.0.0 - project: @tryghost/elasticsearch 4.0.0 - project: @tryghost/jest-snapshot 1.0.0 - project: @tryghost/pretty-stream 1.0.0 - project: @tryghost/express-test 1.0.0 - project: @tryghost/http-stream 1.0.0 - project: @tryghost/job-manager 2.0.0 - project: @tryghost/nodemailer 1.0.0 - project: @tryghost/pretty-cli 2.0.0 - project: @tryghost/root-utils 1.0.0 - project: @tryghost/validator 1.0.0 - project: @tryghost/mw-vhost 2.0.0 - project: @tryghost/security 2.0.0 - project: @tryghost/logging 3.0.0 - project: @tryghost/metrics 2.0.0 - project: @tryghost/promise 1.0.0 - project: @tryghost/request 2.0.0 - project: @tryghost/version 1.0.0 - project: @tryghost/config 1.0.0 - project: @tryghost/errors 2.0.0 - project: @tryghost/server 1.0.0 - project: @tryghost/debug 1.0.0 - project: @tryghost/tpl 1.0.0 - project: @tryghost/zip 2.0.0 --- ghost/api-framework/package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index d92d4b6fd1b..c2cc292be6c 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/api-framework", - "version": "1.0.7", + "version": "2.0.0", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", @@ -29,11 +29,11 @@ "sinon": "21.0.1" }, "dependencies": { - "@tryghost/debug": "^0.1.36", - "@tryghost/errors": "^1.3.9", - "@tryghost/promise": "^0.3.16", - "@tryghost/tpl": "^0.1.36", - "@tryghost/validator": "^0.2.18", + "@tryghost/debug": "^1.0.0", + "@tryghost/errors": "^2.0.0", + "@tryghost/promise": "^1.0.0", + "@tryghost/tpl": "^1.0.0", + "@tryghost/validator": "^1.0.0", "json-stable-stringify": "1.3.0", "lodash": "4.17.23" } From 9e3429953757630bf679ed5df6daab737f132145 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 20:03:19 -0600 Subject: [PATCH 082/118] chore(deps): update test & linting packages (major) (#179) no ref - updated test dependencies - updated nx config accordingly Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Steve Larson <9larsons@gmail.com> --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index c2cc292be6c..7e543810771 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -24,7 +24,7 @@ "lib" ], "devDependencies": { - "c8": "8.0.1", + "c8": "10.1.3", "mocha": "11.7.5", "sinon": "21.0.1" }, From c49348a9724bb05a4e98485c5a3638124ab3285b Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Fri, 20 Feb 2026 20:11:52 -0600 Subject: [PATCH 083/118] Published new versions - project: @tryghost/bookshelf-transaction-events 1.1.0 - project: @tryghost/bookshelf-include-count 1.1.0 - project: @tryghost/bookshelf-custom-query 1.1.0 - project: @tryghost/webhook-mock-receiver 1.1.0 - project: @tryghost/bookshelf-eager-load 1.1.0 - project: @tryghost/bookshelf-pagination 1.1.0 - project: @tryghost/bookshelf-collision 1.1.0 - project: @tryghost/bookshelf-has-posts 1.1.0 - project: @tryghost/email-mock-receiver 1.1.0 - project: @tryghost/prometheus-metrics 2.1.0 - project: @tryghost/bookshelf-plugins 1.1.0 - project: @tryghost/bookshelf-filter 1.1.0 - project: @tryghost/bookshelf-search 1.1.0 - project: @tryghost/http-cache-utils 1.1.0 - project: @tryghost/mw-error-handler 2.1.0 - project: @tryghost/bookshelf-order 1.1.0 - project: @tryghost/api-framework 2.1.0 - project: @tryghost/database-info 1.1.0 - project: @tryghost/domain-events 2.1.0 - project: @tryghost/elasticsearch 4.1.0 - project: @tryghost/jest-snapshot 1.1.0 - project: @tryghost/pretty-stream 1.1.0 - project: @tryghost/express-test 1.1.0 - project: @tryghost/http-stream 1.1.0 - project: @tryghost/job-manager 2.1.0 - project: @tryghost/nodemailer 1.1.0 - project: @tryghost/pretty-cli 2.1.0 - project: @tryghost/root-utils 1.1.0 - project: @tryghost/validator 1.1.0 - project: @tryghost/mw-vhost 2.1.0 - project: @tryghost/security 2.1.0 - project: @tryghost/logging 3.1.0 - project: @tryghost/metrics 2.1.0 - project: @tryghost/promise 1.1.0 - project: @tryghost/request 2.1.0 - project: @tryghost/version 1.1.0 - project: @tryghost/config 1.1.0 - project: @tryghost/errors 2.1.0 - project: @tryghost/server 1.1.0 - project: @tryghost/debug 1.1.0 - project: @tryghost/tpl 1.1.0 - project: @tryghost/zip 2.1.0 --- ghost/api-framework/package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 7e543810771..4be4dea261c 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/api-framework", - "version": "2.0.0", + "version": "2.1.0", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", @@ -29,11 +29,11 @@ "sinon": "21.0.1" }, "dependencies": { - "@tryghost/debug": "^1.0.0", - "@tryghost/errors": "^2.0.0", - "@tryghost/promise": "^1.0.0", - "@tryghost/tpl": "^1.0.0", - "@tryghost/validator": "^1.0.0", + "@tryghost/debug": "^1.1.0", + "@tryghost/errors": "^2.1.0", + "@tryghost/promise": "^1.1.0", + "@tryghost/tpl": "^1.1.0", + "@tryghost/validator": "^1.1.0", "json-stable-stringify": "1.3.0", "lodash": "4.17.23" } From aeca3786986fa0212aa3ce399bad87f0e357b23c Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Fri, 20 Feb 2026 20:34:41 -0600 Subject: [PATCH 084/118] Published new versions - project: @tryghost/bookshelf-transaction-events 1.2.0 - project: @tryghost/bookshelf-include-count 1.2.0 - project: @tryghost/bookshelf-custom-query 1.2.0 - project: @tryghost/webhook-mock-receiver 1.2.0 - project: @tryghost/bookshelf-eager-load 1.2.0 - project: @tryghost/bookshelf-pagination 1.2.0 - project: @tryghost/bookshelf-collision 1.2.0 - project: @tryghost/bookshelf-has-posts 1.2.0 - project: @tryghost/email-mock-receiver 1.2.0 - project: @tryghost/prometheus-metrics 2.2.0 - project: @tryghost/bookshelf-plugins 1.2.0 - project: @tryghost/bookshelf-filter 1.2.0 - project: @tryghost/bookshelf-search 1.2.0 - project: @tryghost/http-cache-utils 1.2.0 - project: @tryghost/mw-error-handler 2.2.0 - project: @tryghost/bookshelf-order 1.2.0 - project: @tryghost/api-framework 2.2.0 - project: @tryghost/database-info 1.2.0 - project: @tryghost/domain-events 2.2.0 - project: @tryghost/elasticsearch 4.2.0 - project: @tryghost/jest-snapshot 1.2.0 - project: @tryghost/pretty-stream 1.2.0 - project: @tryghost/express-test 1.2.0 - project: @tryghost/http-stream 1.2.0 - project: @tryghost/job-manager 2.2.0 - project: @tryghost/nodemailer 1.2.0 - project: @tryghost/pretty-cli 2.2.0 - project: @tryghost/root-utils 1.2.0 - project: @tryghost/validator 1.2.0 - project: @tryghost/mw-vhost 2.2.0 - project: @tryghost/security 2.2.0 - project: @tryghost/logging 3.2.0 - project: @tryghost/metrics 2.2.0 - project: @tryghost/promise 1.2.0 - project: @tryghost/request 2.2.0 - project: @tryghost/version 1.2.0 - project: @tryghost/config 1.2.0 - project: @tryghost/errors 2.2.0 - project: @tryghost/server 1.2.0 - project: @tryghost/debug 1.2.0 - project: @tryghost/tpl 1.2.0 - project: @tryghost/zip 2.2.0 --- ghost/api-framework/package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 4be4dea261c..d9048ed22b8 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/api-framework", - "version": "2.1.0", + "version": "2.2.0", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", @@ -29,11 +29,11 @@ "sinon": "21.0.1" }, "dependencies": { - "@tryghost/debug": "^1.1.0", - "@tryghost/errors": "^2.1.0", - "@tryghost/promise": "^1.1.0", - "@tryghost/tpl": "^1.1.0", - "@tryghost/validator": "^1.1.0", + "@tryghost/debug": "^1.2.0", + "@tryghost/errors": "^2.2.0", + "@tryghost/promise": "^1.2.0", + "@tryghost/tpl": "^1.2.0", + "@tryghost/validator": "^1.2.0", "json-stable-stringify": "1.3.0", "lodash": "4.17.23" } From 1936f786cb457c5b9cf2fa24ec812e5b320f433e Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Fri, 20 Feb 2026 21:01:05 -0600 Subject: [PATCH 085/118] Published new versions - project: @tryghost/bookshelf-transaction-events 1.2.1 - project: @tryghost/bookshelf-include-count 1.2.1 - project: @tryghost/bookshelf-custom-query 1.2.1 - project: @tryghost/webhook-mock-receiver 1.2.1 - project: @tryghost/bookshelf-eager-load 1.2.1 - project: @tryghost/bookshelf-pagination 1.2.1 - project: @tryghost/bookshelf-collision 1.2.1 - project: @tryghost/bookshelf-has-posts 1.2.1 - project: @tryghost/email-mock-receiver 1.2.1 - project: @tryghost/prometheus-metrics 2.2.1 - project: @tryghost/bookshelf-plugins 1.2.1 - project: @tryghost/bookshelf-filter 1.2.1 - project: @tryghost/bookshelf-search 1.2.1 - project: @tryghost/http-cache-utils 1.2.1 - project: @tryghost/mw-error-handler 2.2.1 - project: @tryghost/bookshelf-order 1.2.1 - project: @tryghost/api-framework 2.2.1 - project: @tryghost/database-info 1.2.1 - project: @tryghost/domain-events 2.2.1 - project: @tryghost/elasticsearch 4.2.1 - project: @tryghost/jest-snapshot 1.2.1 - project: @tryghost/pretty-stream 1.2.1 - project: @tryghost/express-test 1.2.1 - project: @tryghost/http-stream 1.2.1 - project: @tryghost/job-manager 2.2.1 - project: @tryghost/nodemailer 1.2.1 - project: @tryghost/pretty-cli 2.2.1 - project: @tryghost/root-utils 1.2.1 - project: @tryghost/validator 1.2.1 - project: @tryghost/mw-vhost 2.2.1 - project: @tryghost/security 2.2.1 - project: @tryghost/logging 3.2.1 - project: @tryghost/metrics 2.2.1 - project: @tryghost/promise 1.2.1 - project: @tryghost/request 2.2.1 - project: @tryghost/version 1.2.1 - project: @tryghost/config 1.2.1 - project: @tryghost/errors 2.2.1 - project: @tryghost/server 1.2.1 - project: @tryghost/debug 1.2.1 - project: @tryghost/tpl 1.2.1 - project: @tryghost/zip 2.2.1 --- ghost/api-framework/package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index d9048ed22b8..22fe978742d 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/api-framework", - "version": "2.2.0", + "version": "2.2.1", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", @@ -29,11 +29,11 @@ "sinon": "21.0.1" }, "dependencies": { - "@tryghost/debug": "^1.2.0", - "@tryghost/errors": "^2.2.0", - "@tryghost/promise": "^1.2.0", - "@tryghost/tpl": "^1.2.0", - "@tryghost/validator": "^1.2.0", + "@tryghost/debug": "^1.2.1", + "@tryghost/errors": "^2.2.1", + "@tryghost/promise": "^1.2.1", + "@tryghost/tpl": "^1.2.1", + "@tryghost/validator": "^1.2.1", "json-stable-stringify": "1.3.0", "lodash": "4.17.23" } From aaf88663fc014968d0aff1991db2e6130b93b1a2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:45:48 +0000 Subject: [PATCH 086/118] Update dependency c8 to v11 (#404) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 22fe978742d..a49877a47e7 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -24,7 +24,7 @@ "lib" ], "devDependencies": { - "c8": "10.1.3", + "c8": "11.0.0", "mocha": "11.7.5", "sinon": "21.0.1" }, From 83b3bdfca5676aab63e0610cc595695fdb8a131c Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Mon, 2 Mar 2026 10:56:36 -0600 Subject: [PATCH 087/118] Published new versions - project: @tryghost/bookshelf-transaction-events 2.0.0 - project: @tryghost/bookshelf-include-count 2.0.0 - project: @tryghost/bookshelf-custom-query 2.0.0 - project: @tryghost/webhook-mock-receiver 2.0.0 - project: @tryghost/bookshelf-eager-load 2.0.0 - project: @tryghost/bookshelf-pagination 2.0.0 - project: @tryghost/bookshelf-collision 2.0.0 - project: @tryghost/bookshelf-has-posts 2.0.0 - project: @tryghost/email-mock-receiver 2.0.0 - project: @tryghost/prometheus-metrics 3.0.0 - project: @tryghost/bookshelf-plugins 2.0.0 - project: @tryghost/bookshelf-filter 2.0.0 - project: @tryghost/bookshelf-search 2.0.0 - project: @tryghost/http-cache-utils 2.0.0 - project: @tryghost/mw-error-handler 3.0.0 - project: @tryghost/bookshelf-order 2.0.0 - project: @tryghost/api-framework 3.0.0 - project: @tryghost/database-info 2.0.0 - project: @tryghost/domain-events 3.0.0 - project: @tryghost/elasticsearch 5.0.0 - project: @tryghost/jest-snapshot 2.0.0 - project: @tryghost/pretty-stream 2.0.0 - project: @tryghost/express-test 2.0.0 - project: @tryghost/http-stream 2.0.0 - project: @tryghost/job-manager 3.0.0 - project: @tryghost/nodemailer 2.0.0 - project: @tryghost/pretty-cli 3.0.0 - project: @tryghost/root-utils 2.0.0 - project: @tryghost/validator 2.0.0 - project: @tryghost/mw-vhost 3.0.0 - project: @tryghost/security 3.0.0 - project: @tryghost/logging 4.0.0 - project: @tryghost/metrics 3.0.0 - project: @tryghost/promise 2.0.0 - project: @tryghost/request 3.0.0 - project: @tryghost/version 2.0.0 - project: @tryghost/config 2.0.0 - project: @tryghost/errors 3.0.0 - project: @tryghost/server 2.0.0 - project: @tryghost/debug 2.0.0 - project: @tryghost/tpl 2.0.0 - project: @tryghost/zip 3.0.0 --- ghost/api-framework/package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index a49877a47e7..18121666752 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/api-framework", - "version": "2.2.1", + "version": "3.0.0", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", @@ -29,11 +29,11 @@ "sinon": "21.0.1" }, "dependencies": { - "@tryghost/debug": "^1.2.1", - "@tryghost/errors": "^2.2.1", - "@tryghost/promise": "^1.2.1", - "@tryghost/tpl": "^1.2.1", - "@tryghost/validator": "^1.2.1", + "@tryghost/debug": "^2.0.0", + "@tryghost/errors": "^3.0.0", + "@tryghost/promise": "^2.0.0", + "@tryghost/tpl": "^2.0.0", + "@tryghost/validator": "^2.0.0", "json-stable-stringify": "1.3.0", "lodash": "4.17.23" } From 4917d75bf5097c8c4186e3c9cdfedb25c1620439 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Mon, 2 Mar 2026 11:51:03 -0600 Subject: [PATCH 088/118] Remove json-stable-stringify from @tryghost/api-framework (#428) Inline JSON.stringify with sorted keys at the single call site where it was used (cache key generation). No need for a separate dependency. --- ghost/api-framework/lib/pipeline.js | 3 +-- ghost/api-framework/package.json | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/ghost/api-framework/lib/pipeline.js b/ghost/api-framework/lib/pipeline.js index 436de80465e..b8787cc53f6 100644 --- a/ghost/api-framework/lib/pipeline.js +++ b/ghost/api-framework/lib/pipeline.js @@ -1,6 +1,5 @@ const debug = require('@tryghost/debug')('pipeline'); const _ = require('lodash'); -const stringify = require('json-stable-stringify'); const errors = require('@tryghost/errors'); const {sequence} = require('@tryghost/promise'); @@ -243,7 +242,7 @@ const pipeline = (apiController, apiUtils, apiType) => { cacheKeyData = await apiImpl.generateCacheKeyData(frame); } - const cacheKey = stringify(cacheKeyData); + const cacheKey = JSON.stringify(cacheKeyData, Object.keys(cacheKeyData).sort()); if (apiImpl.cache) { const response = await apiImpl.cache.get(cacheKey, getResponse); diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 18121666752..b8342927408 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -34,7 +34,6 @@ "@tryghost/promise": "^2.0.0", "@tryghost/tpl": "^2.0.0", "@tryghost/validator": "^2.0.0", - "json-stable-stringify": "1.3.0", "lodash": "4.17.23" } } From 1d061d1c8845b8295e865a440f0ab15691e89b13 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Mon, 2 Mar 2026 15:22:22 -0600 Subject: [PATCH 089/118] Replaced Mocha with Vitest across all packages (#429) - unified all packages to use vitest as the test runner - some packages have explicit overrides to vitest config, to be addressed later (particularly when migrating everything to TypeScript) --- ghost/api-framework/package.json | 3 +- ghost/api-framework/test/http.test.js | 210 ++++++++++++++------------ 2 files changed, 113 insertions(+), 100 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index b8342927408..22988f70bd7 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -13,7 +13,7 @@ "main": "index.js", "scripts": { "dev": "echo \"Implement me!\"", - "test:unit": "NODE_ENV=testing c8 --all --reporter text --reporter cobertura --check-coverage --100 -- mocha --reporter dot './test/**/*.test.js'", + "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", @@ -24,7 +24,6 @@ "lib" ], "devDependencies": { - "c8": "11.0.0", "mocha": "11.7.5", "sinon": "21.0.1" }, diff --git a/ghost/api-framework/test/http.test.js b/ghost/api-framework/test/http.test.js index 3e41407cea7..93b8ec6fd95 100644 --- a/ghost/api-framework/test/http.test.js +++ b/ghost/api-framework/test/http.test.js @@ -65,129 +65,143 @@ describe('HTTP', function () { }); }); - it('api response is fn', function (done) { - const response = sinon.stub().callsFake(function (_req, _res, _next) { - assert.ok(_req); - assert.ok(_res); - assert.ok(_next); - assert.equal(apiImpl.calledOnce, true); - assert.equal(_res.json.called, false); - done(); - }); + it('api response is fn', async function () { + await new Promise((resolve) => { + const response = sinon.stub().callsFake(function (_req, _res, _next) { + assert.ok(_req); + assert.ok(_res); + assert.ok(_next); + assert.equal(apiImpl.calledOnce, true); + assert.equal(_res.json.called, false); + resolve(); + }); - const apiImpl = sinon.stub().resolves(response); - shared.http(apiImpl)(req, res, next); + const apiImpl = sinon.stub().resolves(response); + shared.http(apiImpl)(req, res, next); + }); }); - it('api response is fn (data)', function (done) { - const apiImpl = sinon.stub().resolves('data'); + it('api response is fn (data)', async function () { + await new Promise((resolve) => { + const apiImpl = sinon.stub().resolves('data'); - next.callsFake(done); + next.callsFake(resolve); - res.json.callsFake(function () { - assert.equal(shared.headers.get.calledOnce, true); - assert.equal(res.status.calledOnce, true); - assert.equal(res.send.called, false); - done(); - }); + res.json.callsFake(function () { + assert.equal(shared.headers.get.calledOnce, true); + assert.equal(res.status.calledOnce, true); + assert.equal(res.send.called, false); + resolve(); + }); - shared.http(apiImpl)(req, res, next); + shared.http(apiImpl)(req, res, next); + }); }); - it('handles api key, user and plain text response', function (done) { - req.vhost = null; - req.user = {id: 'user-id'}; - req.api_key = { - get(key) { - return { - id: 'api-key-id', - type: 'admin', - integration_id: 'integration-id' - }[key]; - } - }; - - const apiImpl = sinon.stub().resolves('plain body'); - apiImpl.response = {format: 'plain'}; - apiImpl.statusCode = 201; - - res.send.callsFake(() => { - assert.equal(res.status.calledOnceWithExactly(201), true); - assert.equal(res.headers.constructor, Object); - assert.equal(res.json.called, false); + it('handles api key, user and plain text response', async function () { + await new Promise((resolve) => { + req.vhost = null; + req.user = {id: 'user-id'}; + req.api_key = { + get(key) { + return { + id: 'api-key-id', + type: 'admin', + integration_id: 'integration-id' + }[key]; + } + }; + + const apiImpl = sinon.stub().resolves('plain body'); + apiImpl.response = {format: 'plain'}; + apiImpl.statusCode = 201; + + res.send.callsFake(() => { + assert.equal(res.status.calledOnceWithExactly(201), true); + assert.equal(res.headers.constructor, Object); + 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'); + resolve(); + }); - 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'); - done(); + shared.http(apiImpl)(req, res, next); }); - - shared.http(apiImpl)(req, res, next); }); - it('supports async response format and statusCode function', function (done) { - const apiImpl = sinon.stub().resolves({ok: true}); - apiImpl.statusCode = sinon.stub().returns(204); - apiImpl.response = { - format() { - return Promise.resolve('plain'); - } - }; + it('supports async response format and statusCode function', async function () { + await new Promise((resolve) => { + const apiImpl = sinon.stub().resolves({ok: true}); + apiImpl.statusCode = sinon.stub().returns(204); + apiImpl.response = { + format() { + return Promise.resolve('plain'); + } + }; + + res.send.callsFake(() => { + assert.equal(apiImpl.statusCode.calledOnce, true); + assert.equal(res.status.calledOnceWithExactly(204), true); + resolve(); + }); - res.send.callsFake(() => { - assert.equal(apiImpl.statusCode.calledOnce, true); - assert.equal(res.status.calledOnceWithExactly(204), true); - done(); + shared.http(apiImpl)(req, res, next); }); - - shared.http(apiImpl)(req, res, next); }); - it('supports sync response format function', function (done) { - const apiImpl = sinon.stub().resolves('plain body'); - apiImpl.response = { - format() { - return 'plain'; - } - }; + it('supports sync response format function', async function () { + await new Promise((resolve) => { + const apiImpl = sinon.stub().resolves('plain body'); + apiImpl.response = { + format() { + return 'plain'; + } + }; + + res.send.callsFake(() => { + assert.equal(res.send.calledOnce, true); + assert.equal(res.json.called, false); + resolve(); + }); - res.send.callsFake(() => { - assert.equal(res.send.calledOnce, true); - assert.equal(res.json.called, false); - done(); + shared.http(apiImpl)(req, res, next); }); - - shared.http(apiImpl)(req, res, next); }); - it('passes errors to next with frame options', function (done) { - 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 + it('passes errors to next with frame options', async function () { + await new Promise((resolve) => { + 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 + }); + resolve(); }); - done(); - }); - shared.http(apiImpl)(req, res, next); + shared.http(apiImpl)(req, res, next); + }); }); - it('uses req.url pathname when originalUrl is missing', function (done) { - req.originalUrl = undefined; - req.url = '/ghost/api/content/posts/?include=authors'; + 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'; - const apiImpl = sinon.stub().resolves({}); - res.json.callsFake(() => { - const frame = apiImpl.args[0][0]; - assert.equal(frame.original.url.pathname, '/ghost/api/content/posts/'); - done(); - }); + const apiImpl = sinon.stub().resolves({}); + res.json.callsFake(() => { + const frame = apiImpl.args[0][0]; + assert.equal(frame.original.url.pathname, '/ghost/api/content/posts/'); + resolve(); + }); - shared.http(apiImpl)(req, res, next); + shared.http(apiImpl)(req, res, next); + }); }); }); From c3c588f9e7d8cc590c575596a28010457416d77d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 10:46:37 +0000 Subject: [PATCH 090/118] Update dependency sinon to v21.0.2 (#439) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 22988f70bd7..6936598c194 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -25,7 +25,7 @@ ], "devDependencies": { "mocha": "11.7.5", - "sinon": "21.0.1" + "sinon": "21.0.2" }, "dependencies": { "@tryghost/debug": "^2.0.0", From df50a18e923393604a47a231bdffa9825dafc592 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Tue, 10 Mar 2026 09:09:59 -0500 Subject: [PATCH 091/118] Remove leftover mocha devDependencies from all packages (#449) no ref --- ghost/api-framework/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 6936598c194..a6ee098ba78 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -24,7 +24,6 @@ "lib" ], "devDependencies": { - "mocha": "11.7.5", "sinon": "21.0.2" }, "dependencies": { From 7c703e30ee4649ea7ba52eca13d925f308e47dd2 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Tue, 10 Mar 2026 10:26:14 -0500 Subject: [PATCH 092/118] Cleaned up eslint config (#452) no ref --- ghost/api-framework/.eslintrc.js | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 ghost/api-framework/.eslintrc.js diff --git a/ghost/api-framework/.eslintrc.js b/ghost/api-framework/.eslintrc.js deleted file mode 100644 index c9c1bcb5226..00000000000 --- a/ghost/api-framework/.eslintrc.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/node' - ] -}; From 61445996aeda6f76d119c3a422756f18c7e82cf7 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Tue, 10 Mar 2026 10:46:09 -0500 Subject: [PATCH 093/118] Published new versions - project: @tryghost/bookshelf-transaction-events 2.0.1 - project: @tryghost/bookshelf-include-count 2.0.1 - project: @tryghost/bookshelf-custom-query 2.0.1 - project: @tryghost/webhook-mock-receiver 2.0.1 - project: @tryghost/bookshelf-eager-load 2.0.1 - project: @tryghost/bookshelf-pagination 2.0.1 - project: @tryghost/bookshelf-collision 2.0.1 - project: @tryghost/bookshelf-has-posts 2.0.1 - project: @tryghost/email-mock-receiver 2.0.1 - project: @tryghost/prometheus-metrics 3.0.1 - project: @tryghost/bookshelf-plugins 2.0.1 - project: @tryghost/bookshelf-filter 2.0.1 - project: @tryghost/bookshelf-search 2.0.1 - project: @tryghost/http-cache-utils 2.0.1 - project: @tryghost/mw-error-handler 3.0.1 - project: @tryghost/bookshelf-order 2.0.1 - project: @tryghost/api-framework 3.0.1 - project: @tryghost/database-info 2.0.1 - project: @tryghost/domain-events 3.0.1 - project: @tryghost/elasticsearch 5.0.1 - project: @tryghost/jest-snapshot 2.0.1 - project: @tryghost/pretty-stream 2.0.1 - project: @tryghost/express-test 2.0.1 - project: @tryghost/http-stream 2.0.1 - project: @tryghost/job-manager 3.0.1 - project: @tryghost/nodemailer 2.0.1 - project: @tryghost/pretty-cli 3.0.1 - project: @tryghost/root-utils 2.0.1 - project: @tryghost/validator 2.0.1 - project: @tryghost/mw-vhost 3.0.1 - project: @tryghost/security 3.0.1 - project: @tryghost/logging 4.0.1 - project: @tryghost/metrics 3.0.1 - project: @tryghost/promise 2.0.1 - project: @tryghost/request 3.0.1 - project: @tryghost/version 2.0.1 - project: @tryghost/config 2.0.1 - project: @tryghost/errors 3.0.1 - project: @tryghost/server 2.0.1 - project: @tryghost/debug 2.0.1 - project: @tryghost/tpl 2.0.1 - project: @tryghost/zip 3.0.1 --- ghost/api-framework/package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index a6ee098ba78..7fb542a538b 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/api-framework", - "version": "3.0.0", + "version": "3.0.1", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", @@ -27,11 +27,11 @@ "sinon": "21.0.2" }, "dependencies": { - "@tryghost/debug": "^2.0.0", - "@tryghost/errors": "^3.0.0", - "@tryghost/promise": "^2.0.0", - "@tryghost/tpl": "^2.0.0", - "@tryghost/validator": "^2.0.0", + "@tryghost/debug": "^2.0.1", + "@tryghost/errors": "^3.0.1", + "@tryghost/promise": "^2.0.1", + "@tryghost/tpl": "^2.0.1", + "@tryghost/validator": "^2.0.1", "lodash": "4.17.23" } } From d9e8d6d0756306a0449ca410827153cb2c06a89e Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Wed, 11 Mar 2026 08:54:56 -0500 Subject: [PATCH 094/118] Published new versions - project: @tryghost/bookshelf-transaction-events 2.0.2 - project: @tryghost/bookshelf-include-count 2.0.2 - project: @tryghost/bookshelf-custom-query 2.0.2 - project: @tryghost/webhook-mock-receiver 2.0.2 - project: @tryghost/bookshelf-eager-load 2.0.2 - project: @tryghost/bookshelf-pagination 2.0.2 - project: @tryghost/bookshelf-collision 2.0.2 - project: @tryghost/bookshelf-has-posts 2.0.2 - project: @tryghost/email-mock-receiver 2.0.2 - project: @tryghost/prometheus-metrics 3.0.2 - project: @tryghost/bookshelf-plugins 2.0.2 - project: @tryghost/bookshelf-filter 2.0.2 - project: @tryghost/bookshelf-search 2.0.2 - project: @tryghost/http-cache-utils 2.0.2 - project: @tryghost/mw-error-handler 3.0.2 - project: @tryghost/bookshelf-order 2.0.2 - project: @tryghost/api-framework 3.0.2 - project: @tryghost/database-info 2.0.2 - project: @tryghost/domain-events 3.0.2 - project: @tryghost/elasticsearch 5.0.2 - project: @tryghost/jest-snapshot 2.0.2 - project: @tryghost/pretty-stream 2.0.2 - project: @tryghost/express-test 2.0.2 - project: @tryghost/http-stream 2.0.2 - project: @tryghost/job-manager 3.0.2 - project: @tryghost/nodemailer 2.0.2 - project: @tryghost/pretty-cli 3.0.2 - project: @tryghost/root-utils 2.0.2 - project: @tryghost/validator 2.0.2 - project: @tryghost/mw-vhost 3.0.2 - project: @tryghost/security 3.0.2 - project: @tryghost/logging 4.0.2 - project: @tryghost/metrics 3.0.2 - project: @tryghost/promise 2.0.2 - project: @tryghost/request 3.0.2 - project: @tryghost/version 2.0.2 - project: @tryghost/config 2.0.2 - project: @tryghost/errors 3.0.2 - project: @tryghost/server 2.0.2 - project: @tryghost/debug 2.0.2 - project: @tryghost/tpl 2.0.2 - project: @tryghost/zip 3.0.2 --- ghost/api-framework/package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 7fb542a538b..a68cd178bd4 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/api-framework", - "version": "3.0.1", + "version": "3.0.2", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", @@ -27,11 +27,11 @@ "sinon": "21.0.2" }, "dependencies": { - "@tryghost/debug": "^2.0.1", - "@tryghost/errors": "^3.0.1", - "@tryghost/promise": "^2.0.1", - "@tryghost/tpl": "^2.0.1", - "@tryghost/validator": "^2.0.1", + "@tryghost/debug": "^2.0.2", + "@tryghost/errors": "^3.0.2", + "@tryghost/promise": "^2.0.2", + "@tryghost/tpl": "^2.0.2", + "@tryghost/validator": "^2.0.2", "lodash": "4.17.23" } } From 4e56e58e2b5f4c93b3c820fc95a893a5f9dc9ba7 Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Mon, 16 Mar 2026 09:56:29 +0000 Subject: [PATCH 095/118] Published new versions - project: @tryghost/bookshelf-transaction-events 2.0.3 - project: @tryghost/bookshelf-include-count 2.0.3 - project: @tryghost/bookshelf-custom-query 2.0.3 - project: @tryghost/webhook-mock-receiver 2.0.3 - project: @tryghost/bookshelf-eager-load 2.0.3 - project: @tryghost/bookshelf-pagination 2.0.3 - project: @tryghost/bookshelf-collision 2.0.3 - project: @tryghost/bookshelf-has-posts 2.1.0 - project: @tryghost/email-mock-receiver 2.0.3 - project: @tryghost/prometheus-metrics 3.0.3 - project: @tryghost/bookshelf-plugins 2.0.3 - project: @tryghost/bookshelf-filter 2.0.3 - project: @tryghost/bookshelf-search 2.0.3 - project: @tryghost/http-cache-utils 2.0.3 - project: @tryghost/mw-error-handler 3.0.3 - project: @tryghost/bookshelf-order 2.0.3 - project: @tryghost/api-framework 3.0.3 - project: @tryghost/database-info 2.0.3 - project: @tryghost/domain-events 3.0.3 - project: @tryghost/elasticsearch 5.0.3 - project: @tryghost/jest-snapshot 2.0.3 - project: @tryghost/pretty-stream 2.0.3 - project: @tryghost/express-test 2.0.3 - project: @tryghost/http-stream 2.0.3 - project: @tryghost/job-manager 3.0.3 - project: @tryghost/nodemailer 2.0.3 - project: @tryghost/pretty-cli 3.0.3 - project: @tryghost/root-utils 2.0.3 - project: @tryghost/validator 2.0.3 - project: @tryghost/mw-vhost 3.0.3 - project: @tryghost/security 3.0.3 - project: @tryghost/logging 4.0.3 - project: @tryghost/metrics 3.0.3 - project: @tryghost/promise 2.0.3 - project: @tryghost/request 3.0.3 - project: @tryghost/version 2.0.3 - project: @tryghost/config 2.0.3 - project: @tryghost/errors 3.0.3 - project: @tryghost/server 2.0.3 - project: @tryghost/debug 2.0.3 - project: @tryghost/tpl 2.0.3 - project: @tryghost/zip 3.0.3 --- ghost/api-framework/package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index a68cd178bd4..169c2daf54c 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/api-framework", - "version": "3.0.2", + "version": "3.0.3", "repository": { "type": "git", "url": "git+https://github.com/TryGhost/framework.git", @@ -27,11 +27,11 @@ "sinon": "21.0.2" }, "dependencies": { - "@tryghost/debug": "^2.0.2", - "@tryghost/errors": "^3.0.2", - "@tryghost/promise": "^2.0.2", - "@tryghost/tpl": "^2.0.2", - "@tryghost/validator": "^2.0.2", + "@tryghost/debug": "^2.0.3", + "@tryghost/errors": "^3.0.3", + "@tryghost/promise": "^2.0.3", + "@tryghost/tpl": "^2.0.3", + "@tryghost/validator": "^2.0.3", "lodash": "4.17.23" } } From df19aad730985cb612ca36408281b5cf487ae1c0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:04:11 +0000 Subject: [PATCH 096/118] Update dependency sinon to v21.0.3 (#466) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 169c2daf54c..265b5a428be 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -24,7 +24,7 @@ "lib" ], "devDependencies": { - "sinon": "21.0.2" + "sinon": "21.0.3" }, "dependencies": { "@tryghost/debug": "^2.0.3", From 5251c0a1d7ea5271a1b5b1ab54762531bfca8240 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:04:04 +0000 Subject: [PATCH 097/118] Update dependency lodash to v4.18.1 [SECURITY] (#501) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 265b5a428be..cae6cbf1ca1 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -32,6 +32,6 @@ "@tryghost/promise": "^2.0.3", "@tryghost/tpl": "^2.0.3", "@tryghost/validator": "^2.0.3", - "lodash": "4.17.23" + "lodash": "4.18.1" } } From 0bfeb3ce0b2fc99e4584d152c1d7d062d023bb4e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 18:30:03 +0000 Subject: [PATCH 098/118] Update dependency sinon to v21.1.0 (#525) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index cae6cbf1ca1..0482d51e430 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -24,7 +24,7 @@ "lib" ], "devDependencies": { - "sinon": "21.0.3" + "sinon": "21.1.0" }, "dependencies": { "@tryghost/debug": "^2.0.3", From 7436071e522aa480dc1822d5bb4a1439f0ccc510 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 07:48:32 +0000 Subject: [PATCH 099/118] Update dependency sinon to v21.1.1 (#532) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 0482d51e430..3899e6651cf 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -24,7 +24,7 @@ "lib" ], "devDependencies": { - "sinon": "21.1.0" + "sinon": "21.1.1" }, "dependencies": { "@tryghost/debug": "^2.0.3", From 44fd97af848981fc989a36e5441f80a66bf2300c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 22:16:45 +0000 Subject: [PATCH 100/118] Update dependency sinon to v21.1.2 (#536) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 3899e6651cf..7a81431b454 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -24,7 +24,7 @@ "lib" ], "devDependencies": { - "sinon": "21.1.1" + "sinon": "21.1.2" }, "dependencies": { "@tryghost/debug": "^2.0.3", From ea5c67fb065733fc191f37fc50926ef2c3c5ab48 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Tue, 21 Apr 2026 10:27:51 -0500 Subject: [PATCH 101/118] Switched from eslint to oxlint + oxfmt (#544) ref https://linear.app/ghost/issue/PLA-12/ - migrated from eslint to oxfmt/oxlint This PR moves this repo over to oxfmt/oxlint. We are keeping the default rules and not applying the Ghost-specific rules (these will be evaluated later as a separate effort, on whether or not we want to bring them back). The idea here is to update tooling to a faster, modern tool and leverage **defaults**. The only rule we have adjusted here is single quotes. --- ghost/api-framework/README.md | 6 +- ghost/api-framework/lib/api-framework.js | 2 +- ghost/api-framework/lib/headers.js | 36 +- ghost/api-framework/lib/http.js | 21 +- ghost/api-framework/lib/pipeline.js | 31 +- ghost/api-framework/lib/serializers/handle.js | 8 +- ghost/api-framework/lib/serializers/index.js | 2 +- .../lib/serializers/input/all.js | 2 +- .../lib/serializers/input/index.js | 2 +- ghost/api-framework/lib/utils/index.js | 2 +- ghost/api-framework/lib/utils/options.js | 4 +- ghost/api-framework/lib/validators/handle.js | 2 +- ghost/api-framework/lib/validators/index.js | 2 +- .../api-framework/lib/validators/input/all.js | 121 ++++--- .../lib/validators/input/index.js | 2 +- ghost/api-framework/package.json | 22 +- ghost/api-framework/test/.eslintrc.js | 6 - ghost/api-framework/test/frame.test.js | 50 +-- ghost/api-framework/test/headers.test.js | 189 ++++++----- ghost/api-framework/test/http.test.js | 31 +- ghost/api-framework/test/pipeline.test.js | 319 ++++++++++-------- .../test/serializers/handle.test.js | 302 ++++++++++------- .../test/serializers/input/all.test.js | 18 +- ghost/api-framework/test/util/options.test.js | 9 +- .../test/validators/handle.test.js | 27 +- .../test/validators/input/all.test.js | 302 +++++++++-------- 26 files changed, 845 insertions(+), 673 deletions(-) delete mode 100644 ghost/api-framework/test/.eslintrc.js diff --git a/ghost/api-framework/README.md b/ghost/api-framework/README.md index 61ebbe7dac7..c8088742e39 100644 --- a/ghost/api-framework/README.md +++ b/ghost/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/ghost/api-framework/lib/api-framework.js b/ghost/api-framework/lib/api-framework.js index 28fa942163d..f0845664c30 100644 --- a/ghost/api-framework/lib/api-framework.js +++ b/ghost/api-framework/lib/api-framework.js @@ -25,5 +25,5 @@ module.exports = { get utils() { return require('./utils'); - } + }, }; diff --git a/ghost/api-framework/lib/headers.js b/ghost/api-framework/lib/headers.js index 165baeb377a..66ee43c0485 100644 --- a/ghost/api-framework/lib/headers.js +++ b/ghost/api-framework/lib/headers.js @@ -6,7 +6,7 @@ const cacheInvalidate = (result, options = {}) => { let value = options.value; return { - 'X-Cache-Invalidate': value || INVALIDATE_ALL + 'X-Cache-Invalidate': value || INVALIDATE_ALL, }; }; @@ -27,7 +27,7 @@ const disposition = { return { 'Content-Disposition': `Attachment; filename="${value}"`, - 'Content-Type': 'text/csv' + 'Content-Type': 'text/csv', }; }, @@ -42,7 +42,7 @@ const disposition = { return { 'Content-Disposition': `Attachment; filename="${options.value}"`, 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(JSON.stringify(result)) + 'Content-Length': Buffer.byteLength(JSON.stringify(result)), }; }, @@ -57,7 +57,7 @@ const disposition = { return { 'Content-Disposition': `Attachment; filename="${options.value}"`, 'Content-Type': 'application/yaml', - 'Content-Length': Buffer.byteLength(JSON.stringify(result)) + 'Content-Length': Buffer.byteLength(JSON.stringify(result)), }; }, @@ -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,13 +129,17 @@ 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); + let locationURL = url.resolve( + `${protocol}${frame.original.url.host}`, + frame.original.url.pathname, + ); if (!locationURL.endsWith('/')) { locationURL += '/'; } @@ -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/ghost/api-framework/lib/http.js b/ghost/api-framework/lib/http.js index cf35d705591..7d65ac61815 100644 --- a/ghost/api-framework/lib/http.js +++ b/ghost/api-framework/lib/http.js @@ -29,10 +29,10 @@ const http = (apiImpl) => { if (req.api_key) { apiKey = { id: req.api_key.get('id'), - type: req.api_key.get('type') + type: req.api_key.get('type'), }; integration = { - id: req.api_key.get('integration_id') + id: req.api_key.get('integration_id'), }; } @@ -51,19 +51,19 @@ const http = (apiImpl) => { url: { 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 { @@ -87,7 +87,7 @@ 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) => { @@ -102,11 +102,12 @@ const http = (apiImpl) => { let responseFormat; - if (apiImpl.response){ + 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/ghost/api-framework/lib/pipeline.js b/ghost/api-framework/lib/pipeline.js index b8787cc53f6..2e4c3fb4cc9 100644 --- a/ghost/api-framework/lib/pipeline.js +++ b/ghost/api-framework/lib/pipeline.js @@ -1,7 +1,7 @@ const debug = require('@tryghost/debug')('pipeline'); const _ = require('lodash'); const errors = require('@tryghost/errors'); -const {sequence} = require('@tryghost/promise'); +const { sequence } = require('@tryghost/promise'); const Frame = require('./Frame'); const serializers = require('./serializers'); @@ -37,12 +37,12 @@ const STAGES = { return validators.handle.input( Object.assign({}, apiConfig, apiImpl.validation), apiUtils.validators.input, - frame + frame, ); }); return sequence(tasks); - } + }, }, serialisation: { @@ -63,9 +63,9 @@ const STAGES = { input(apiUtils, apiConfig, apiImpl, frame) { 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, ); }, @@ -85,8 +85,13 @@ const STAGES = { */ output(response, apiUtils, apiConfig, apiImpl, frame) { debug('stages: output serialisation'); - return serializers.handle.output(response, apiConfig, apiUtils.serializers.output, frame); - } + return serializers.handle.output( + response, + apiConfig, + apiUtils.serializers.output, + frame, + ); + }, }, /** @@ -132,7 +137,7 @@ const STAGES = { tasks.push(function doPermissions() { return apiUtils.permissions.handle( Object.assign({}, apiConfig, apiImpl.permissions), - frame + frame, ); }); @@ -156,7 +161,7 @@ const STAGES = { } 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; @@ -216,12 +221,12 @@ const pipeline = (apiController, apiUtils, apiType) => { frame = new Frame({ body: data, options: _.omit(options, 'context'), - context: options.context || {} + context: options.context || {}, }); frame.configure({ options: apiImpl.options, - data: apiImpl.data + data: apiImpl.data, }); } else { frame = options; diff --git a/ghost/api-framework/lib/serializers/handle.js b/ghost/api-framework/lib/serializers/handle.js index 6356ebdbf35..94b8539968b 100644 --- a/ghost/api-framework/lib/serializers/handle.js +++ b/ghost/api-framework/lib/serializers/handle.js @@ -1,5 +1,5 @@ const debug = require('@tryghost/debug')('serializers:handle'); -const {sequence} = require('@tryghost/promise'); +const { sequence } = require('@tryghost/promise'); const errors = require('@tryghost/errors'); /** @@ -113,7 +113,11 @@ module.exports.output = (response = {}, apiConfig, apiSerializers, frame) => { }); } - const customSerializer = getBestMatchSerializer(apiSerializers, apiConfig.docName, apiConfig.method); + const customSerializer = getBestMatchSerializer( + apiSerializers, + apiConfig.docName, + apiConfig.method, + ); const defaultSerializer = getBestMatchSerializer(apiSerializers, 'default', apiConfig.method); if (customSerializer) { diff --git a/ghost/api-framework/lib/serializers/index.js b/ghost/api-framework/lib/serializers/index.js index ba10b3cbeeb..8fed3415ab7 100644 --- a/ghost/api-framework/lib/serializers/index.js +++ b/ghost/api-framework/lib/serializers/index.js @@ -9,5 +9,5 @@ module.exports = { get output() { return require('./output'); - } + }, }; diff --git a/ghost/api-framework/lib/serializers/input/all.js b/ghost/api-framework/lib/serializers/input/all.js index 4a2a0ddad4a..c4b347f0b13 100644 --- a/ghost/api-framework/lib/serializers/input/all.js +++ b/ghost/api-framework/lib/serializers/input/all.js @@ -37,5 +37,5 @@ module.exports = { debug('omit internal options'); frame.options = _.omit(frame.options, INTERNAL_OPTIONS); } - } + }, }; diff --git a/ghost/api-framework/lib/serializers/input/index.js b/ghost/api-framework/lib/serializers/input/index.js index 7b25869f4f5..3a8190073bf 100644 --- a/ghost/api-framework/lib/serializers/input/index.js +++ b/ghost/api-framework/lib/serializers/input/index.js @@ -1,5 +1,5 @@ module.exports = { get all() { return require('./all'); - } + }, }; diff --git a/ghost/api-framework/lib/utils/index.js b/ghost/api-framework/lib/utils/index.js index ce0fc5c4305..78db2d7fa91 100644 --- a/ghost/api-framework/lib/utils/index.js +++ b/ghost/api-framework/lib/utils/index.js @@ -1,5 +1,5 @@ module.exports = { get options() { return require('./options'); - } + }, }; diff --git a/ghost/api-framework/lib/utils/options.js b/ghost/api-framework/lib/utils/options.js index 90b5d6eaa45..8badb97b14f 100644 --- a/ghost/api-framework/lib/utils/options.js +++ b/ghost/api-framework/lib/utils/options.js @@ -1,5 +1,5 @@ const _ = require('lodash'); -const {IncorrectUsageError} = require('@tryghost/errors'); +const { IncorrectUsageError } = require('@tryghost/errors'); /** * @description Helper function to prepare params for internal usages. @@ -20,7 +20,7 @@ const trimAndLowerCase = (params) => { // 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/ghost/api-framework/lib/validators/handle.js b/ghost/api-framework/lib/validators/handle.js index 43f62f77ddc..5eb642be212 100644 --- a/ghost/api-framework/lib/validators/handle.js +++ b/ghost/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 { sequence } = require('@tryghost/promise'); /** * @description Shared input validation handler. diff --git a/ghost/api-framework/lib/validators/index.js b/ghost/api-framework/lib/validators/index.js index 7830fcfee49..95c9b221dfa 100644 --- a/ghost/api-framework/lib/validators/index.js +++ b/ghost/api-framework/lib/validators/index.js @@ -5,5 +5,5 @@ module.exports = { get input() { return require('./input'); - } + }, }; diff --git a/ghost/api-framework/lib/validators/input/all.js b/ghost/api-framework/lib/validators/input/all.js index 661600db54d..77d155b2ec4 100644 --- a/ghost/api-framework/lib/validators/input/all.js +++ b/ghost/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 { 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.' + 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, + }), + }), + ); } }); @@ -63,7 +65,9 @@ const validate = (config, attrs) => { 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); }); @@ -71,16 +75,18 @@ const validate = (config, attrs) => { 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)); + 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, + }), + }), + ); } } } @@ -139,10 +145,16 @@ module.exports = { // 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 ( + _.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), + }), + }), + ); } } }, @@ -195,11 +211,16 @@ module.exports = { // 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 ( + 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), + }), + ); } } }, @@ -222,5 +243,5 @@ module.exports = { publish() { debug('validate schedule'); return this.browse(...arguments); - } + }, }; diff --git a/ghost/api-framework/lib/validators/input/index.js b/ghost/api-framework/lib/validators/input/index.js index 7b25869f4f5..3a8190073bf 100644 --- a/ghost/api-framework/lib/validators/input/index.js +++ b/ghost/api-framework/lib/validators/input/index.js @@ -1,5 +1,5 @@ module.exports = { get all() { return require('./all'); - } + }, }; diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 7a81431b454..ecb27cab1b7 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,30 +1,25 @@ { "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", "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" - }, - "files": [ - "index.js", - "lib" - ], - "devDependencies": { - "sinon": "21.1.2" + "lint": "oxlint -c ../../.oxlintrc.json ." }, "dependencies": { "@tryghost/debug": "^2.0.3", @@ -33,5 +28,8 @@ "@tryghost/tpl": "^2.0.3", "@tryghost/validator": "^2.0.3", "lodash": "4.18.1" + }, + "devDependencies": { + "sinon": "21.1.2" } } diff --git a/ghost/api-framework/test/.eslintrc.js b/ghost/api-framework/test/.eslintrc.js deleted file mode 100644 index 829b601eb0a..00000000000 --- a/ghost/api-framework/test/.eslintrc.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: ['ghost'], - extends: [ - 'plugin:ghost/test' - ] -}; diff --git a/ghost/api-framework/test/frame.test.js b/ghost/api-framework/test/frame.test.js index 8a42a4c0b6d..0cc183a5bcb 100644 --- a/ghost/api-framework/test/frame.test.js +++ b/ghost/api-framework/test/frame.test.js @@ -14,17 +14,17 @@ describe('Frame', function () { 'apiType', 'docName', 'method', - 'response' + 'response', ]); }); 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); @@ -42,16 +42,16 @@ describe('Frame', 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); @@ -65,16 +65,16 @@ describe('Frame', 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); @@ -83,17 +83,17 @@ describe('Frame', 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); @@ -103,10 +103,10 @@ describe('Frame', 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); @@ -117,7 +117,7 @@ describe('Frame', function () { }, data() { return ['slug', 'id']; - } + }, }); assert.equal(frame.options.include, 'tags'); @@ -133,10 +133,10 @@ describe('Frame', function () { 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'}); + assert.deepEqual(frame.getHeaders(), { 'X-Test': '1' }); }); }); }); diff --git a/ghost/api-framework/test/headers.test.js b/ghost/api-framework/test/headers.test.js index 250a53c2cb0..bf26dce27ba 100644 --- a/ghost/api-framework/test/headers.test.js +++ b/ghost/api-framework/test/headers.test.js @@ -11,73 +11,88 @@ describe('Headers', function () { describe('config.disposition', function () { it('json', function () { - return shared.headers.get({}, {disposition: {type: 'json', value: 'value'}}, new Frame()) + 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-Length': 2, }); }); }); it('csv', function () { - return shared.headers.get({}, {disposition: {type: 'csv', value: 'my.csv'}}, new Frame()) + 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-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()); + 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-Type': 'text/csv', }); }); it('file', async function () { - const result = await shared.headers.get({}, {disposition: {type: 'file', value: 'my.txt'}}, new Frame()); + 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()); + 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()) + 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-Length': 11, }); }); }); @@ -85,19 +100,19 @@ describe('Headers', function () { describe('config.cacheInvalidate', function () { it('default', function () { - return shared.headers.get({}, {cacheInvalidate: true}, new Frame()) - .then((result) => { - assert.deepEqual(result, { - 'X-Cache-Invalidate': '/*' - }); + 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()) + return shared.headers + .get({}, { cacheInvalidate: { value: 'value' } }, new Frame()) .then((result) => { assert.deepEqual(result, { - 'X-Cache-Invalidate': 'value' + 'X-Cache-Invalidate': 'value', }); }); }); @@ -106,36 +121,39 @@ describe('Headers', 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.docName = 'posts'; + frame.method = 'add'; frame.original = { url: { host: 'example.com', - pathname: `/api/content/posts/` - } + 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 () { const apiResult = { - posts: [{ - id: 'id_value' - }] + posts: [ + { + id: 'id_value', + }, + ], }; const resolvedLocationUrl = 'resolved location'; @@ -144,8 +162,8 @@ describe('Headers', function () { location: { resolve() { return resolvedLocationUrl; - } - } + }, + }, }; const frame = new Frame(); frame.docName = 'posts'; @@ -153,23 +171,24 @@ describe('Headers', function () { frame.original = { url: { host: 'example.com', - pathname: `/api/content/posts/existing_post_id_value/copy` - } + 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 () { const apiResult = { - posts: [{ - id: 'id_value' - }] + posts: [ + { + id: 'id_value', + }, + ], }; const apiConfigHeaders = {}; @@ -181,22 +200,24 @@ describe('Headers', function () { url: { 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 () { const apiResult = { - posts: [{ - id: 'id_value' - }] + posts: [ + { + id: 'id_value', + }, + ], }; const apiConfigHeaders = {}; @@ -206,17 +227,16 @@ describe('Headers', function () { frame.original = { url: { host: 'example.com', - pathname: `/api/content/posts` - } + 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 () { @@ -229,14 +249,13 @@ describe('Headers', function () { frame.original = { url: { host: 'example.com', - pathname: `/api/content/posts/` - } + 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/ghost/api-framework/test/http.test.js b/ghost/api-framework/test/http.test.js index 93b8ec6fd95..4e27ee5418c 100644 --- a/ghost/api-framework/test/http.test.js +++ b/ghost/api-framework/test/http.test.js @@ -13,16 +13,15 @@ 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.secure = true; - req.url = 'https://example.com/ghost/api/content/', - + req.url = 'https://example.com/ghost/api/content/'; res.status = sinon.stub(); res.json = sinon.stub(); res.set = (headers) => { @@ -51,17 +50,17 @@ describe('HTTP', function () { 'apiType', 'docName', 'method', - 'response' + '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, + }, }); }); @@ -101,19 +100,19 @@ describe('HTTP', 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' + integration_id: 'integration-id', }[key]; - } + }, }; const apiImpl = sinon.stub().resolves('plain body'); - apiImpl.response = {format: 'plain'}; + apiImpl.response = { format: 'plain' }; apiImpl.statusCode = 201; res.send.callsFake(() => { @@ -134,12 +133,12 @@ describe('HTTP', 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'); - } + }, }; res.send.callsFake(() => { @@ -158,7 +157,7 @@ describe('HTTP', function () { apiImpl.response = { format() { return 'plain'; - } + }, }; res.send.callsFake(() => { @@ -180,7 +179,7 @@ describe('HTTP', function () { assert.equal(err, error); assert.deepEqual(req.frameOptions, { docName: null, - method: null + method: null, }); resolve(); }); diff --git a/ghost/api-framework/test/pipeline.test.js b/ghost/api-framework/test/pipeline.test.js index b0e7e88e84a..811bce02709 100644 --- a/ghost/api-framework/test/pipeline.test.js +++ b/ghost/api-framework/test/pipeline.test.js @@ -19,11 +19,12 @@ describe('Pipeline', 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'); @@ -36,45 +37,50 @@ describe('Pipeline', function () { const apiUtils = { validators: { input: { - posts: {} - } - } + posts: {}, + }, + }, }; const apiConfig = { - docName: 'posts' + docName: 'posts', }; const apiImpl = { 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, + ); }); }); }); @@ -84,18 +90,19 @@ describe('Pipeline', 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' + method: 'browse', }); }); }); @@ -103,15 +110,24 @@ describe('Pipeline', function () { 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, + ); }); }); }); @@ -122,8 +138,8 @@ describe('Pipeline', function () { beforeEach(function () { apiUtils = { permissions: { - handle: sinon.stub().resolves() - } + handle: sinon.stub().resolves(), + }, }; }); @@ -143,69 +159,77 @@ describe('Pipeline', 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) => { + 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 () { 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 () { 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 () { 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 () { @@ -213,16 +237,17 @@ describe('Pipeline', function () { 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); - }); + }, + ); }); }); @@ -238,11 +263,10 @@ describe('Pipeline', function () { 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); + }); }); }); }); @@ -259,7 +283,7 @@ describe('Pipeline', function () { it('ensure we receive a callable api controller fn', function () { const apiController = { add: {}, - browse: {} + browse: {}, }; const apiUtils = {}; @@ -275,7 +299,7 @@ describe('Pipeline', function () { it('call api controller fn', function () { const apiController = { - add: {} + add: {}, }; const apiUtils = {}; @@ -285,20 +309,21 @@ describe('Pipeline', function () { 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.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 () { @@ -307,8 +332,8 @@ describe('Pipeline', function () { add: { headers: {}, permissions: true, - query: sinon.stub().resolves('response') - } + query: sinon.stub().resolves('response'), + }, }; const apiUtils = {}; @@ -318,15 +343,18 @@ describe('Pipeline', function () { 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.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 }); }); }); @@ -336,8 +364,8 @@ describe('Pipeline', function () { add: { headers: {}, permissions: true, - query: sinon.stub().resolves('response') - } + query: sinon.stub().resolves('response'), + }, }; const apiUtils = {}; @@ -347,37 +375,37 @@ describe('Pipeline', function () { 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.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 () { const apiController = { add() { 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 () { @@ -385,9 +413,9 @@ describe('Pipeline', function () { 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 = {}; @@ -398,14 +426,19 @@ describe('Pipeline', function () { 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.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( + apiController.browse.generateCacheKeyData.calledOnceWithExactly(frame), + true, + ); assert.equal(frame.apiType, 'content'); assert.equal(frame.docName, undefined); assert.equal(frame.method, 'browse'); @@ -417,8 +450,8 @@ describe('Pipeline', function () { browse: { headers: {}, permissions: true, - query: sinon.stub().resolves('response') - } + query: sinon.stub().resolves('response'), + }, }; const first = shared.pipeline(apiController, {}); @@ -442,9 +475,9 @@ describe('Pipeline', function () { browse: { cache: { get: sinon.stub().resolves(null), - set: sinon.stub().resolves(true) - } - } + set: sinon.stub().resolves(true), + }, + }, }; const apiUtils = {}; @@ -454,9 +487,11 @@ describe('Pipeline', function () { 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.serialisation.output.callsFake( + function (response, _apiUtils, apiConfig, apiImpl, frame) { + frame.response = response; + }, + ); const response = await result.browse(); @@ -479,9 +514,9 @@ describe('Pipeline', function () { browse: { cache: { get: sinon.stub().resolves('CACHED RESPONSE'), - set: sinon.stub().resolves(true) - } - } + set: sinon.stub().resolves(true), + }, + }, }; const apiUtils = {}; @@ -491,9 +526,11 @@ describe('Pipeline', function () { 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.serialisation.output.callsFake( + function (response, _apiUtils, apiConfig, apiImpl, frame) { + frame.response = response; + }, + ); const response = await result.browse(); diff --git a/ghost/api-framework/test/serializers/handle.test.js b/ghost/api-framework/test/serializers/handle.test.js index 06f5f8d8e77..e7af944073f 100644 --- a/ghost/api-framework/test/serializers/handle.test.js +++ b/ghost/api-framework/test/serializers/handle.test.js @@ -10,7 +10,8 @@ describe('serializers/handle', function () { describe('input', function () { it('no api config passed', function () { - return shared.serializers.handle.input() + return shared.serializers.handle + .input() .then(Promise.reject) .catch((err) => { assert.equal(err instanceof errors.IncorrectUsageError, true); @@ -18,7 +19,8 @@ describe('serializers/handle', function () { }); it('no api serializers passed', function () { - return shared.serializers.handle.input({}) + return shared.serializers.handle + .input({}) .then(Promise.reject) .catch((err) => { assert.equal(err instanceof errors.IncorrectUsageError, true); @@ -33,26 +35,25 @@ describe('serializers/handle', function () { 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 () { @@ -67,11 +68,11 @@ describe('serializers/handle', function () { 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,29 +80,31 @@ 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; + let apiSerializers, response, apiConfig, frame; beforeEach(function () { response = []; - apiConfig = {docName: 'posts', method: 'add'}; + apiConfig = { docName: 'posts', method: 'add' }; frame = {}; }); @@ -110,7 +113,8 @@ describe('serializers/handle', function () { }); it('no api config passed', function () { - return shared.serializers.handle.output([]) + return shared.serializers.handle + .output([]) .then(Promise.reject) .catch((err) => { assert.equal(err instanceof errors.IncorrectUsageError, true); @@ -118,7 +122,8 @@ describe('serializers/handle', function () { }); it('no api serializers passed', function () { - return shared.serializers.handle.output([], {}) + return shared.serializers.handle + .output([], {}) .then(Promise.reject) .catch((err) => { assert.equal(err instanceof errors.IncorrectUsageError, true); @@ -129,26 +134,33 @@ describe('serializers/handle', 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) + 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'}; + 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); @@ -161,55 +173,66 @@ describe('serializers/handle', 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 - ]; + 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'}; + 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); }); @@ -221,36 +244,40 @@ describe('serializers/handle', 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 - ]; + 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); @@ -258,23 +285,29 @@ describe('serializers/handle', function () { }); it('uses nearest fallback serializer when custom match does not exist', function () { - apiConfig = {docName: 'posts', method: 'idontexist'}; + 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); @@ -287,37 +320,41 @@ describe('serializers/handle', 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 - ]; + 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); @@ -326,23 +363,29 @@ describe('serializers/handle', function () { }); it('uses nearest fallback serializer when custom match does not exist', function () { - apiConfig = {docName: 'posts', method: 'idontexist'}; + 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); @@ -356,54 +399,65 @@ describe('serializers/handle', 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 - ]; + 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'}; + 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/ghost/api-framework/test/serializers/input/all.test.js b/ghost/api-framework/test/serializers/input/all.test.js index 379848d9584..fcf73265022 100644 --- a/ghost/api-framework/test/serializers/input/all.test.js +++ b/ghost/api-framework/test/serializers/input/all.test.js @@ -9,14 +9,14 @@ describe('serializers/input/all', function () { original: { include: 'tags', fields: 'id,status', - formats: 'html' + formats: 'html', }, options: { include: 'tags', fields: 'id,status', formats: 'html', - context: {} - } + context: {}, + }, }; shared.serializers.input.all.all(apiConfig, frame); @@ -41,11 +41,11 @@ describe('serializers/input/all', function () { const frame = { options: { context: { - internal: true + internal: true, }, transacting: true, - forUpdate: true - } + forUpdate: true, + }, }; const apiConfig = {}; @@ -61,11 +61,11 @@ describe('serializers/input/all', function () { const frame = { options: { context: { - user: true + user: true, }, transacting: true, - forUpdate: true - } + forUpdate: true, + }, }; const apiConfig = {}; diff --git a/ghost/api-framework/test/util/options.test.js b/ghost/api-framework/test/util/options.test.js index 2dbb4bcd75d..be5ed83b847 100644 --- a/ghost/api-framework/test/util/options.test.js +++ b/ghost/api-framework/test/util/options.test.js @@ -19,12 +19,15 @@ describe('util/options', function () { }); it('accepts parameters in form of an array', function () { - assert.deepEqual(optionsUtil.trimAndLowerCase([' PeanUt', ' buTTer ']), ['peanut', 'butter']); + 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' + assert.throws(() => optionsUtil.trimAndLowerCase({ name: 'peanut' }), { + message: 'Params must be a string or array', }); }); }); diff --git a/ghost/api-framework/test/validators/handle.test.js b/ghost/api-framework/test/validators/handle.test.js index 86421f12acc..ce079e83863 100644 --- a/ghost/api-framework/test/validators/handle.test.js +++ b/ghost/api-framework/test/validators/handle.test.js @@ -10,7 +10,8 @@ describe('validators/handle', function () { describe('input', function () { it('no api config passed', function () { - return shared.validators.handle.input() + return shared.validators.handle + .input() .then(Promise.reject) .catch((err) => { assert.equal(err instanceof errors.IncorrectUsageError, true); @@ -18,7 +19,8 @@ describe('validators/handle', function () { }); it('no api validators passed', function () { - return shared.validators.handle.input({}) + return shared.validators.handle + .input({}) .then(Promise.reject) .catch((err) => { assert.equal(err instanceof errors.IncorrectUsageError, true); @@ -26,7 +28,8 @@ describe('validators/handle', function () { }); it('no api config passed when validators exist', function () { - return shared.validators.handle.input(undefined, {}, {}) + return shared.validators.handle + .input(undefined, {}, {}) .then(Promise.reject) .catch((err) => { assert.equal(err instanceof errors.IncorrectUsageError, true); @@ -45,17 +48,18 @@ describe('validators/handle', function () { 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); @@ -68,11 +72,12 @@ describe('validators/handle', 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/ghost/api-framework/test/validators/input/all.test.js b/ghost/api-framework/test/validators/input/all.test.js index 66febed0e16..92500229597 100644 --- a/ghost/api-framework/test/validators/input/all.test.js +++ b/ghost/api-framework/test/validators/input/all.test.js @@ -15,63 +15,64 @@ describe('validators/input/all', function () { context: {}, slug: 'slug', include: 'tags,authors', - page: 2 - } + page: 2, + }, }; const apiConfig = { options: { include: { values: ['tags', 'authors'], - required: true - } - } + 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); - }); + 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 () { 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(() => { + return shared.validators.input.all.all(apiConfig, frame).then( + () => { throw new Error('Should not resolve'); - }, (err) => { + }, + (err) => { assert.ok(err); - }); + }, + ); }); 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); @@ -83,26 +84,25 @@ describe('validators/input/all', function () { context: {}, slug: 'slug', include: ['tags', 'authors'], - page: 2 - } + page: 2, + }, }; const apiConfig = { options: { include: { values: ['tags', 'authors'], - required: true - } - } + 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); - }); + 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 () { @@ -111,42 +111,42 @@ describe('validators/input/all', function () { context: {}, slug: 'slug', include: 'tags,authors', - page: 2 - } + 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 () { 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); @@ -157,17 +157,18 @@ describe('validators/input/all', 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); @@ -177,19 +178,20 @@ describe('validators/input/all', 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); @@ -200,13 +202,14 @@ describe('validators/input/all', 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); @@ -217,17 +220,18 @@ describe('validators/input/all', 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); @@ -240,11 +244,11 @@ describe('validators/input/all', function () { it('default', function () { const frame = { options: { - context: {} + context: {}, }, data: { - status: 'aus' - } + status: 'aus', + }, }; const apiConfig = {}; @@ -257,16 +261,17 @@ describe('validators/input/all', 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); @@ -280,8 +285,8 @@ describe('validators/input/all', function () { const frame = { options: { - context: {} - } + context: {}, + }, }; const apiConfig = {}; @@ -294,14 +299,15 @@ describe('validators/input/all', 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); @@ -311,15 +317,16 @@ describe('validators/input/all', 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); @@ -329,22 +336,25 @@ describe('validators/input/all', function () { it('fails for required field', function () { const frame = { data: { - docName: [{ - a: 'b' - }] - } + docName: [ + { + a: 'b', + }, + ], + }, }; const apiConfig = { 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); @@ -355,23 +365,26 @@ describe('validators/input/all', function () { it('fails for invalid field', function () { const frame = { data: { - docName: [{ - a: 'b', - b: null - }] - } + docName: [ + { + a: 'b', + b: null, + }, + ], + }, }; const apiConfig = { 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); @@ -382,14 +395,16 @@ describe('validators/input/all', 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); @@ -400,23 +415,24 @@ describe('validators/input/all', 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); @@ -424,8 +440,11 @@ describe('validators/input/all', function () { }); 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({}, {}) + 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'); @@ -435,16 +454,20 @@ describe('validators/input/all', function () { 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'}] - } - }) + 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); @@ -454,14 +477,17 @@ describe('validators/input/all', function () { 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'}] - } - }); + const result = shared.validators.input.all.edit( + { + docName: 'posts', + }, + { + options: { id: 'id-1' }, + data: { + posts: [{ id: 'id-2' }], + }, + }, + ); assert.equal(result, undefined); }); }); From e8b2dd869cb19ed8c41660110644ec20614e1e6d Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Tue, 21 Apr 2026 16:20:35 -0500 Subject: [PATCH 102/118] Migrated from yarn to pnpm 10.33 (#549) no ref Migrated to pnpm 10.33 to match TryGhost/Ghost. --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index ecb27cab1b7..62daf614649 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -18,7 +18,7 @@ "scripts": { "dev": "echo \"Implement me!\"", "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "test": "yarn test:unit", + "test": "pnpm run test:unit", "lint": "oxlint -c ../../.oxlintrc.json ." }, "dependencies": { From 9887986c485dcc56f055a03fd3d1a4e37e1b5915 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Wed, 22 Apr 2026 07:07:02 -0500 Subject: [PATCH 103/118] Converted internal deps to pnpm workspace: protocol (#554) no ref - Convert every internal @tryghost/* ref in dependencies/devDependencies to workspace:*. pnpm pack rewrites this to the real version at publish time, so consumers outside the monorepo are unaffected. - Declare @tryghost/debug in prometheus-metrics (phantom import from MetricsServer.ts that only resolved under yarn's flat layout). - Declare @types/node as a direct devDep on the two TypeScript packages (errors, prometheus-metrics). These were the only packages relying on the hoisted @types/node to satisfy the shared tsconfig's types:[node]. - Delete .npmrc. link-workspace-packages and both public-hoist-pattern entries are no longer needed. --- ghost/api-framework/package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 62daf614649..9e435d669be 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -22,11 +22,11 @@ "lint": "oxlint -c ../../.oxlintrc.json ." }, "dependencies": { - "@tryghost/debug": "^2.0.3", - "@tryghost/errors": "^3.0.3", - "@tryghost/promise": "^2.0.3", - "@tryghost/tpl": "^2.0.3", - "@tryghost/validator": "^2.0.3", + "@tryghost/debug": "workspace:*", + "@tryghost/errors": "workspace:*", + "@tryghost/promise": "workspace:*", + "@tryghost/tpl": "workspace:*", + "@tryghost/validator": "workspace:*", "lodash": "4.18.1" }, "devDependencies": { From 2267afb4f718ae40136e4e895f6819e8e8d7c7f5 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Wed, 22 Apr 2026 07:18:49 -0500 Subject: [PATCH 104/118] Move pnpm config to workspace.yaml, catalog shared devDeps (#557) no ref Some more pnpm optimizations: - Move overrides and onlyBuiltDependencies from root package.json's pnpm key into pnpm-workspace.yaml. This is where pnpm 10 recommends these live, alongside the packages glob. - Introduce a default catalog for three devDeps that were duplicated across packages at identical versions: sinon (35 packages + root) ts-node (2 packages + root) typescript (2 packages) Every reference becomes "catalog:". Bumping any of these is now a one-line change in pnpm-workspace.yaml instead of a 35-file PR. typescript and ts-node will grow further with the ts-esm-migration, so cataloguing them now avoids having to re-touch each manifest later. pnpm pack continues to rewrite "catalog:" to real versions at publish time (verified: @tryghost/api-framework pack emits "sinon": "21.1.2"), so consumers outside the monorepo are unaffected. --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 9e435d669be..eed83a78e7e 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -30,6 +30,6 @@ "lodash": "4.18.1" }, "devDependencies": { - "sinon": "21.1.2" + "sinon": "catalog:" } } From 3b0d9d4517ac1c4218f024650d6cbbb660ba2373 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Wed, 22 Apr 2026 07:41:19 -0500 Subject: [PATCH 105/118] Updated package READMEs: yarn -> pnpm, eslint -> oxlint, lerna -> Nx (#558) no ref Sweep across all package READMEs to bring install/develop/test instructions in line with the current toolchain. No code changes. --- ghost/api-framework/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ghost/api-framework/README.md b/ghost/api-framework/README.md index c8088742e39..99767311c5a 100644 --- a/ghost/api-framework/README.md +++ b/ghost/api-framework/README.md @@ -142,9 +142,9 @@ 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. +2. Run `pnpm install` to install top-level dependencies. ## Test -- `yarn lint` run just eslint -- `yarn test` run lint and tests +- `pnpm lint` runs oxlint +- `pnpm test` runs lint and tests From 055825fc57c1bc9cdfeee048f8c1a625d22dc703 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Wed, 22 Apr 2026 09:40:03 -0500 Subject: [PATCH 106/118] Published new versions - project: @tryghost/bookshelf-transaction-events 2.1.0 - project: @tryghost/bookshelf-include-count 2.1.0 - project: @tryghost/bookshelf-custom-query 2.1.0 - project: @tryghost/webhook-mock-receiver 2.1.0 - project: @tryghost/bookshelf-eager-load 2.1.0 - project: @tryghost/bookshelf-pagination 2.1.0 - project: @tryghost/bookshelf-collision 2.1.0 - project: @tryghost/bookshelf-has-posts 2.2.0 - project: @tryghost/email-mock-receiver 2.1.0 - project: @tryghost/prometheus-metrics 3.1.0 - project: @tryghost/bookshelf-plugins 2.1.0 - project: @tryghost/bookshelf-filter 2.1.0 - project: @tryghost/bookshelf-search 2.1.0 - project: @tryghost/http-cache-utils 2.1.0 - project: @tryghost/mw-error-handler 3.1.0 - project: @tryghost/bookshelf-order 2.1.0 - project: @tryghost/api-framework 3.1.0 - project: @tryghost/database-info 2.1.0 - project: @tryghost/domain-events 3.1.0 - project: @tryghost/elasticsearch 5.1.0 - project: @tryghost/jest-snapshot 2.1.0 - project: @tryghost/pretty-stream 2.1.0 - project: @tryghost/express-test 2.1.0 - project: @tryghost/http-stream 2.1.0 - project: @tryghost/job-manager 3.1.0 - project: @tryghost/nodemailer 2.1.0 - project: @tryghost/pretty-cli 3.1.0 - project: @tryghost/root-utils 2.1.0 - project: @tryghost/validator 2.1.0 - project: @tryghost/mw-vhost 3.1.0 - project: @tryghost/security 3.1.0 - project: @tryghost/logging 4.1.0 - project: @tryghost/metrics 3.1.0 - project: @tryghost/promise 2.1.0 - project: @tryghost/request 3.1.0 - project: @tryghost/version 2.1.0 - project: @tryghost/config 2.1.0 - project: @tryghost/errors 3.1.0 - project: @tryghost/server 2.1.0 - project: @tryghost/debug 2.1.0 - project: @tryghost/tpl 2.1.0 - project: @tryghost/zip 3.1.0 --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index eed83a78e7e..d5043bea8ad 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/api-framework", - "version": "3.0.3", + "version": "3.1.0", "author": "Ghost Foundation", "repository": { "type": "git", From 9f6a44a2e33840a96284991195c89cfbd4d524cd Mon Sep 17 00:00:00 2001 From: Rob Lester Date: Thu, 23 Apr 2026 13:35:42 +0100 Subject: [PATCH 107/118] Fixed cache key serialization dropping nested object keys (#561) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ref ONC-1676 The pipeline built its cache key with `JSON.stringify(cacheKeyData, Object.keys(cacheKeyData).sort())`. The array form of the replacer is a recursive key whitelist — it is applied at every depth, not just the top level — so any key in a nested object that did not also appear as a top-level key of `cacheKeyData` was silently dropped from the serialized output. As a result, two calls whose `cacheKeyData` agreed on the top-level keys but differed inside a nested object produced the same cache key. Replaced the replacer array with a `sortKeysReplacer` function that returns each plain object with its keys sorted while leaving the values intact, so the output stays deterministic at every depth and no nested keys are lost. Added a regression test in `pipeline.test.js` that exercises two requests with identical top-level keys but different nested fields and asserts the resulting cache keys are distinct. --- ghost/api-framework/lib/pipeline.js | 17 +++- ghost/api-framework/test/pipeline.test.js | 106 ++++++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) diff --git a/ghost/api-framework/lib/pipeline.js b/ghost/api-framework/lib/pipeline.js index 2e4c3fb4cc9..186b644f141 100644 --- a/ghost/api-framework/lib/pipeline.js +++ b/ghost/api-framework/lib/pipeline.js @@ -7,6 +7,21 @@ const Frame = require('./Frame'); const serializers = require('./serializers'); const validators = require('./validators'); +// Replacer for JSON.stringify that returns every plain object with its keys +// sorted, so the serialized output is deterministic at every depth. Unlike an +// array replacer — which acts as a recursive key whitelist and silently drops +// any key not present in the top-level list — this preserves all nested keys. +function sortKeysReplacer(_key, value) { + if (value && typeof value === 'object' && !Array.isArray(value)) { + const sorted = {}; + for (const k of Object.keys(value).sort()) { + sorted[k] = value[k]; + } + return sorted; + } + return value; +} + const STAGES = { validation: { /** @@ -247,7 +262,7 @@ const pipeline = (apiController, apiUtils, apiType) => { cacheKeyData = await apiImpl.generateCacheKeyData(frame); } - const cacheKey = JSON.stringify(cacheKeyData, Object.keys(cacheKeyData).sort()); + const cacheKey = JSON.stringify(cacheKeyData, sortKeysReplacer); if (apiImpl.cache) { const response = await apiImpl.cache.get(cacheKey, getResponse); diff --git a/ghost/api-framework/test/pipeline.test.js b/ghost/api-framework/test/pipeline.test.js index 811bce02709..59c17ec1fa0 100644 --- a/ghost/api-framework/test/pipeline.test.js +++ b/ghost/api-framework/test/pipeline.test.js @@ -546,5 +546,111 @@ describe('Pipeline', function () { // cache not set assert.equal(apiController.browse.cache.set.calledOnce, false); }); + + it('produces distinct cache keys when cacheKeyData objects differ only in nested fields', async function () { + const cache = { + get: sinon.stub().resolves(null), + set: sinon.stub().resolves(true), + }; + + const apiController = { + browse: { + cache, + generateCacheKeyData: sinon.stub(), + }, + }; + + const apiUtils = {}; + const result = shared.pipeline(apiController, apiUtils); + + 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; + }, + ); + + // Two requests that share every top-level key but differ deep inside + // a nested object must not collide. Top-level keys are identical on + // purpose — that is what the replacer-array form silently dropped. + apiController.browse.generateCacheKeyData.onFirstCall().resolves({ + options: { filter: 'status:published', limit: 15 }, + auth: { free: true, tiers: [] }, + method: 'browse', + }); + apiController.browse.generateCacheKeyData.onSecondCall().resolves({ + options: { filter: 'status:published', limit: 15 }, + auth: { free: false, tiers: ['gold'] }, + method: 'browse', + }); + + await result.browse(); + await result.browse(); + + const firstKey = cache.get.firstCall.args[0]; + const secondKey = cache.get.secondCall.args[0]; + + assert.notEqual( + firstKey, + secondKey, + 'nested-field differences must produce distinct cache keys', + ); + }); + + it('produces identical cache keys when cacheKeyData objects are reordered at any depth', async function () { + const cache = { + get: sinon.stub().resolves(null), + set: sinon.stub().resolves(true), + }; + + const apiController = { + browse: { + cache, + generateCacheKeyData: sinon.stub(), + }, + }; + + const apiUtils = {}; + const result = shared.pipeline(apiController, apiUtils); + + 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; + }, + ); + + // Same logical payload, keys inserted in different orders at every + // depth (top-level, options, auth, and an object nested inside an + // array). Insertion order must not affect the cache key. + apiController.browse.generateCacheKeyData.onFirstCall().resolves({ + options: { filter: 'status:published', limit: 15 }, + auth: { free: false, tiers: [{ slug: 'gold', order: 1 }] }, + method: 'browse', + }); + apiController.browse.generateCacheKeyData.onSecondCall().resolves({ + method: 'browse', + auth: { tiers: [{ order: 1, slug: 'gold' }], free: false }, + options: { limit: 15, filter: 'status:published' }, + }); + + await result.browse(); + await result.browse(); + + const firstKey = cache.get.firstCall.args[0]; + const secondKey = cache.get.secondCall.args[0]; + + assert.equal( + firstKey, + secondKey, + 'key insertion order must not affect the cache key', + ); + }); }); }); From c32aa6f52566eb88ff3945012cd81a3a8cc73619 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Thu, 23 Apr 2026 08:31:19 -0500 Subject: [PATCH 108/118] Tightened CI workflows (#563) no ref - concurrency groups cancel stale PR runs and serialize publishes - format:check gate catches oxfmt drift before it lands - .nx/cache is restored between CI runs for cache hits on re-runs - drop redundant npm 11 install (Node 24 ships with it) - drop duplicated pnpm version pin (packageManager field drives it) - reformat 5 test files that had pre-existing oxfmt drift so the new gate is green --- ghost/api-framework/test/pipeline.test.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ghost/api-framework/test/pipeline.test.js b/ghost/api-framework/test/pipeline.test.js index 59c17ec1fa0..891427659ff 100644 --- a/ghost/api-framework/test/pipeline.test.js +++ b/ghost/api-framework/test/pipeline.test.js @@ -646,11 +646,7 @@ describe('Pipeline', function () { const firstKey = cache.get.firstCall.args[0]; const secondKey = cache.get.secondCall.args[0]; - assert.equal( - firstKey, - secondKey, - 'key insertion order must not affect the cache key', - ); + assert.equal(firstKey, secondKey, 'key insertion order must not affect the cache key'); }); }); }); From 1ba0e8245b5b5d462a6dcd6004d288071dacb3a9 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Thu, 23 Apr 2026 09:57:36 -0500 Subject: [PATCH 109/118] Standardize per-package test scripts (#570) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every package's test script is now the same shape: "test": "NODE_ENV=testing vitest run --coverage" Three kinds of drift collapsed: - "--config ../../vitest.config.ts" flag removed (vitest auto-discovers the root vitest.config.ts from a package dir — verified behavior is identical with and without the flag) - Vestigial test:unit indirection removed from 8 packages that had test: "pnpm run test:unit" → test:unit: "vitest ..." with no other sub-targets. The indirection was left over from an aspirational unit/integration split that never happened. - prometheus-metrics keeps its test: "test:types && test:unit" compound since it has a legitimate tsc --noEmit type check alongside tests --- ghost/api-framework/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index d5043bea8ad..85c97a4658f 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -17,8 +17,7 @@ }, "scripts": { "dev": "echo \"Implement me!\"", - "test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts", - "test": "pnpm run test:unit", + "test": "NODE_ENV=testing vitest run --coverage", "lint": "oxlint -c ../../.oxlintrc.json ." }, "dependencies": { From 98b06b19227895563de445a34f0b8f992c3ba99a Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Thu, 23 Apr 2026 14:17:43 -0500 Subject: [PATCH 110/118] Published new versions - project: @tryghost/prometheus-metrics 3.1.1 - project: @tryghost/api-framework 3.1.1 - project: @tryghost/domain-events 3.1.1 - project: @tryghost/http-stream 2.1.1 - project: @tryghost/job-manager 3.1.1 - project: @tryghost/validator 3.0.0 - project: @tryghost/logging 4.1.1 - project: @tryghost/request 3.1.1 - project: @tryghost/server 2.1.1 --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 85c97a4658f..488fb13fcea 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/api-framework", - "version": "3.1.0", + "version": "3.1.1", "author": "Ghost Foundation", "repository": { "type": "git", From 02489db6d1ec5a2fce67bd1184a6adf1ffbda5e5 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Mon, 27 Apr 2026 10:39:51 -0500 Subject: [PATCH 111/118] Published new versions - project: @tryghost/bookshelf-transaction-events 2.2.0 - project: @tryghost/bookshelf-include-count 2.2.0 - project: @tryghost/bookshelf-custom-query 2.2.0 - project: @tryghost/webhook-mock-receiver 2.2.0 - project: @tryghost/bookshelf-eager-load 2.2.0 - project: @tryghost/bookshelf-pagination 2.2.0 - project: @tryghost/bookshelf-collision 2.2.0 - project: @tryghost/bookshelf-has-posts 2.3.0 - project: @tryghost/email-mock-receiver 2.2.0 - project: @tryghost/prometheus-metrics 3.2.0 - project: @tryghost/bookshelf-plugins 2.2.0 - project: @tryghost/bookshelf-filter 2.2.0 - project: @tryghost/bookshelf-search 2.2.0 - project: @tryghost/http-cache-utils 2.2.0 - project: @tryghost/mw-error-handler 3.2.0 - project: @tryghost/bookshelf-order 2.2.0 - project: @tryghost/api-framework 3.2.0 - project: @tryghost/database-info 2.2.0 - project: @tryghost/domain-events 3.2.0 - project: @tryghost/elasticsearch 5.2.0 - project: @tryghost/jest-snapshot 2.2.0 - project: @tryghost/pretty-stream 2.2.0 - project: @tryghost/express-test 2.2.0 - project: @tryghost/http-stream 2.2.0 - project: @tryghost/job-manager 3.2.0 - project: @tryghost/nodemailer 2.2.0 - project: @tryghost/pretty-cli 3.2.0 - project: @tryghost/root-utils 2.2.0 - project: @tryghost/validator 3.1.0 - project: @tryghost/mw-vhost 3.2.0 - project: @tryghost/security 3.2.0 - project: @tryghost/logging 4.2.0 - project: @tryghost/metrics 3.2.0 - project: @tryghost/promise 2.2.0 - project: @tryghost/request 3.2.0 - project: @tryghost/version 2.2.0 - project: @tryghost/config 2.2.0 - project: @tryghost/errors 3.2.0 - project: @tryghost/server 2.2.0 - project: @tryghost/debug 2.2.0 - project: @tryghost/tpl 2.2.0 - project: @tryghost/zip 3.2.0 --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 488fb13fcea..e6fc93757fc 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/api-framework", - "version": "3.1.1", + "version": "3.2.0", "author": "Ghost Foundation", "repository": { "type": "git", From b5e37a92b2aaeb3b4a916bf88a313b7275014429 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Thu, 14 May 2026 12:43:59 +0100 Subject: [PATCH 112/118] Published new versions - project: @tryghost/bookshelf-pagination 2.2.1 - project: @tryghost/bookshelf-collision 2.2.1 - project: @tryghost/prometheus-metrics 3.2.1 - project: @tryghost/bookshelf-plugins 2.2.1 - project: @tryghost/bookshelf-filter 2.2.1 - project: @tryghost/mw-error-handler 3.2.1 - project: @tryghost/api-framework 3.2.1 - project: @tryghost/domain-events 3.2.1 - project: @tryghost/jest-snapshot 2.2.1 - project: @tryghost/express-test 2.2.1 - project: @tryghost/http-stream 2.2.1 - project: @tryghost/job-manager 3.2.1 - project: @tryghost/nodemailer 2.2.1 - project: @tryghost/validator 3.1.1 - project: @tryghost/logging 4.2.1 - project: @tryghost/request 3.2.1 - project: @tryghost/errors 3.2.1 - project: @tryghost/server 2.2.1 - project: @tryghost/zip 3.3.1 --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index e6fc93757fc..288cb3591f0 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/api-framework", - "version": "3.2.0", + "version": "3.2.1", "author": "Ghost Foundation", "repository": { "type": "git", From 3aac1d8efaf8dab412404fa2a6f2e56bae96eaba Mon Sep 17 00:00:00 2001 From: Sam Lord Date: Tue, 26 May 2026 15:47:16 +0100 Subject: [PATCH 113/118] Published new versions - project: @tryghost/bookshelf-transaction-events 2.2.1 - project: @tryghost/bookshelf-include-count 2.2.1 - project: @tryghost/bookshelf-custom-query 2.2.1 - project: @tryghost/webhook-mock-receiver 2.2.1 - project: @tryghost/bookshelf-eager-load 2.2.1 - project: @tryghost/bookshelf-pagination 2.2.2 - project: @tryghost/bookshelf-collision 2.2.2 - project: @tryghost/bookshelf-has-posts 2.3.1 - project: @tryghost/email-mock-receiver 2.2.1 - project: @tryghost/prometheus-metrics 4.0.1 - project: @tryghost/bookshelf-plugins 2.2.2 - project: @tryghost/bookshelf-filter 2.2.2 - project: @tryghost/bookshelf-search 2.2.1 - project: @tryghost/http-cache-utils 2.2.1 - project: @tryghost/mw-error-handler 3.2.2 - project: @tryghost/bookshelf-order 2.2.1 - project: @tryghost/api-framework 3.2.2 - project: @tryghost/database-info 2.2.1 - project: @tryghost/domain-events 3.2.3 - project: @tryghost/elasticsearch 5.2.1 - project: @tryghost/jest-snapshot 2.2.2 - project: @tryghost/pretty-stream 2.2.1 - project: @tryghost/express-test 2.2.2 - project: @tryghost/http-stream 2.2.2 - project: @tryghost/job-manager 4.0.2 - project: @tryghost/nodemailer 2.2.2 - project: @tryghost/pretty-cli 3.2.1 - project: @tryghost/root-utils 2.2.1 - project: @tryghost/validator 3.1.2 - project: @tryghost/mw-vhost 3.2.1 - project: @tryghost/security 3.2.1 - project: @tryghost/logging 5.0.1 - project: @tryghost/metrics 3.2.1 - project: @tryghost/promise 2.2.1 - project: @tryghost/request 3.2.2 - project: @tryghost/version 2.2.1 - project: @tryghost/config 2.2.1 - project: @tryghost/errors 3.2.2 - project: @tryghost/server 3.0.1 - project: @tryghost/debug 2.2.1 - project: @tryghost/tpl 2.2.1 - project: @tryghost/zip 3.3.2 --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 288cb3591f0..5653e3386a3 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/api-framework", - "version": "3.2.1", + "version": "3.2.2", "author": "Ghost Foundation", "repository": { "type": "git", From 9925cd29cc87f49fbab3374b1ef1a62ec13bc82b Mon Sep 17 00:00:00 2001 From: Jonatan Svennberg Date: Mon, 1 Jun 2026 13:48:46 +0200 Subject: [PATCH 114/118] Updated API input validators (#689) Kept request parameter handling aligned with the documented accepted values. --- .../api-framework/lib/validators/input/all.js | 4 +- .../test/validators/input/all.test.js | 72 +++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/ghost/api-framework/lib/validators/input/all.js b/ghost/api-framework/lib/validators/input/all.js index 77d155b2ec4..c27e77a70b0 100644 --- a/ghost/api-framework/lib/validators/input/all.js +++ b/ghost/api-framework/lib/validators/input/all.js @@ -11,9 +11,9 @@ const messages = { }; const GLOBAL_VALIDATORS = { - id: { matches: /^[a-f\d]{24}$|^1$|me/i }, + id: { matches: /^(?:[a-f\d]{24}|1|me)$/i }, page: { matches: /^\d+$/ }, - limit: { matches: /^\d+|all$/ }, + limit: { matches: /^(?:\d+|all)$/ }, from: { isDate: true }, to: { isDate: true }, columns: { matches: /^[\w, ]+$/ }, diff --git a/ghost/api-framework/test/validators/input/all.test.js b/ghost/api-framework/test/validators/input/all.test.js index 92500229597..4f20b2e22e3 100644 --- a/ghost/api-framework/test/validators/input/all.test.js +++ b/ghost/api-framework/test/validators/input/all.test.js @@ -216,6 +216,78 @@ describe('validators/input/all', function () { }); }); + it('allows supported id values', async function () { + const ids = ['a'.repeat(24), '1', 'me', 'ME']; + + for (const id of ids) { + const frame = { + options: { + context: {}, + id, + }, + }; + + await shared.validators.input.all.all({}, frame); + } + }); + + it('rejects id values that contain me as a substring', async function () { + const ids = [ + "aaaaaaaaaaaaaaaaaaaaaaaa',member_id:'aaaaaaaaaaaaaaaaaaaaaaaa", + 'member_id', + 'not-me', + 'me123', + '123me', + ]; + + for (const id of ids) { + const frame = { + options: { + context: {}, + id, + }, + }; + + await assert.rejects(shared.validators.input.all.all({}, frame), (err) => { + assert.equal(err.message, 'Validation (matches) failed for id'); + return true; + }); + } + }); + + it('allows supported limit values', async function () { + const limits = ['10', '0', 'all']; + + for (const limit of limits) { + const frame = { + options: { + context: {}, + limit, + }, + }; + + await shared.validators.input.all.all({}, frame); + } + }); + + it('rejects limit values that only partially match supported values', async function () { + const limits = ['10junk', 'junkall', '10all']; + + for (const limit of limits) { + const frame = { + options: { + context: {}, + limit, + }, + }; + + await assert.rejects(shared.validators.input.all.all({}, frame), (err) => { + assert.equal(err.message, 'Validation (matches) failed for limit'); + return true; + }); + } + }); + it('fails on invalid allowed values for non-include fields', function () { const frame = { options: { From 0dadff6fca8fc8b050a5e3a7041c7021cfbcac17 Mon Sep 17 00:00:00 2001 From: Jonatan Svennberg Date: Mon, 1 Jun 2026 13:54:59 +0200 Subject: [PATCH 115/118] Published new versions --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 5653e3386a3..85394f55d89 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/api-framework", - "version": "3.2.2", + "version": "3.2.3", "author": "Ghost Foundation", "repository": { "type": "git", From 7ec1e306d92bf456cd73c6f058b16c374eb19760 Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Fri, 12 Jun 2026 18:14:22 +0100 Subject: [PATCH 116/118] Published new versions - project: @tryghost/bookshelf-transaction-events 2.2.3 - project: @tryghost/bookshelf-include-count 2.2.3 - project: @tryghost/bookshelf-custom-query 2.2.3 - project: @tryghost/webhook-mock-receiver 2.2.3 - project: @tryghost/bookshelf-eager-load 2.2.3 - project: @tryghost/bookshelf-pagination 2.2.4 - project: @tryghost/bookshelf-collision 2.2.4 - project: @tryghost/bookshelf-has-posts 2.3.3 - project: @tryghost/email-mock-receiver 2.2.3 - project: @tryghost/prometheus-metrics 4.0.3 - project: @tryghost/bookshelf-plugins 2.2.4 - project: @tryghost/bookshelf-filter 2.2.4 - project: @tryghost/bookshelf-search 2.2.3 - project: @tryghost/http-cache-utils 2.2.3 - project: @tryghost/mw-error-handler 3.2.4 - project: @tryghost/bookshelf-order 2.2.3 - project: @tryghost/api-framework 3.2.4 - project: @tryghost/database-info 2.2.3 - project: @tryghost/domain-events 3.2.5 - project: @tryghost/elasticsearch 5.2.3 - project: @tryghost/jest-snapshot 2.2.4 - project: @tryghost/pretty-stream 2.2.3 - project: @tryghost/express-test 2.2.4 - project: @tryghost/http-stream 2.2.4 - project: @tryghost/job-manager 4.0.4 - project: @tryghost/nodemailer 2.2.4 - project: @tryghost/pretty-cli 3.2.3 - project: @tryghost/root-utils 2.2.3 - project: @tryghost/validator 3.1.4 - project: @tryghost/mw-vhost 3.2.3 - project: @tryghost/security 3.2.3 - project: @tryghost/logging 5.0.3 - project: @tryghost/metrics 3.2.3 - project: @tryghost/promise 2.2.3 - project: @tryghost/request 3.2.4 - project: @tryghost/version 2.2.3 - project: @tryghost/config 2.2.3 - project: @tryghost/errors 3.2.4 - project: @tryghost/server 3.0.3 - project: @tryghost/debug 2.2.3 - project: @tryghost/tpl 2.2.3 - project: @tryghost/zip 3.3.4 --- ghost/api-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 85394f55d89..63acabf40d1 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/api-framework", - "version": "3.2.3", + "version": "3.2.4", "author": "Ghost Foundation", "repository": { "type": "git", From cdeaee3d86ed9a9b733e88e16f6401a7389c0ffa Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Thu, 18 Jun 2026 05:03:47 +0000 Subject: [PATCH 117/118] Integrated @tryghost/api-framework into the pnpm + Nx workspace - pointed ghost/core at the workspace package (workspace:*) and removed the now-unused catalog entry, so the in-tree package is the single source - mirrored ghost/core's dependency specs (debug + lodash via catalog, errors/promise/tpl/validator pinned) to avoid installing duplicate copies - swapped the framework repo's oxlint script + root vitest inheritance for a local vitest config and test:unit/test targets so Nx discovers and runs the suite; eslint lint wiring left as a follow-up - marked the package private since it is no longer published from the framework repo --- ghost/api-framework/package.json | 34 +++++--------- ghost/api-framework/vitest.config.mjs | 12 +++++ ghost/core/package.json | 2 +- pnpm-lock.yaml | 66 +++++++++++++-------------- pnpm-workspace.yaml | 1 - vitest.config.mjs | 1 + 6 files changed, 58 insertions(+), 58 deletions(-) create mode 100644 ghost/api-framework/vitest.config.mjs diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 63acabf40d1..fa64045e20e 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -1,34 +1,24 @@ { "name": "@tryghost/api-framework", - "version": "3.2.4", + "version": "0.0.0", + "private": true, "author": "Ghost Foundation", - "repository": { - "type": "git", - "url": "git+https://github.com/TryGhost/framework.git", - "directory": "packages/api-framework" - }, - "files": [ - "index.js", - "lib" - ], "main": "index.js", - "publishConfig": { - "access": "public" - }, "scripts": { "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing vitest run --coverage", - "lint": "oxlint -c ../../.oxlintrc.json ." + "test:unit": "NODE_ENV=testing vitest run", + "test": "pnpm test:unit" }, "dependencies": { - "@tryghost/debug": "workspace:*", - "@tryghost/errors": "workspace:*", - "@tryghost/promise": "workspace:*", - "@tryghost/tpl": "workspace:*", - "@tryghost/validator": "workspace:*", - "lodash": "4.18.1" + "@tryghost/debug": "catalog:", + "@tryghost/errors": "1.3.13", + "@tryghost/promise": "2.2.3", + "@tryghost/tpl": "2.2.3", + "@tryghost/validator": "0.2.22", + "lodash": "catalog:" }, "devDependencies": { - "sinon": "catalog:" + "sinon": "catalog:", + "vitest": "catalog:" } } diff --git a/ghost/api-framework/vitest.config.mjs b/ghost/api-framework/vitest.config.mjs new file mode 100644 index 00000000000..feda138291d --- /dev/null +++ b/ghost/api-framework/vitest.config.mjs @@ -0,0 +1,12 @@ +import {defineConfig} from 'vitest/config'; + +// Local config so `vitest run` in this package runs only its own tests rather +// than the monorepo root project list. Tests rely on globals (describe/it). +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['test/**/*.test.{js,ts}'], + pool: 'threads' + } +}); diff --git a/ghost/core/package.json b/ghost/core/package.json index 1e4dad5f1c4..ddfb7964a55 100644 --- a/ghost/core/package.json +++ b/ghost/core/package.json @@ -92,7 +92,7 @@ "@slack/webhook": "7.0.9", "@tryghost/adapter-base-cache": "0.1.26", "@tryghost/admin-api-schema": "4.7.5", - "@tryghost/api-framework": "catalog:", + "@tryghost/api-framework": "workspace:*", "@tryghost/bookshelf-plugins": "2.2.4", "@tryghost/color-utils": "catalog:", "@tryghost/config-url-helpers": "1.0.26", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0453019b888..21bcef93d8e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,9 +72,6 @@ catalogs: '@testing-library/react': specifier: 14.3.1 version: 14.3.1 - '@tryghost/api-framework': - specifier: 3.2.4 - version: 3.2.4 '@tryghost/color-utils': specifier: 0.2.19 version: 0.2.19 @@ -2312,6 +2309,34 @@ importers: specifier: 4.0.2 version: 4.0.2 + ghost/api-framework: + dependencies: + '@tryghost/debug': + specifier: 'catalog:' + version: 2.2.3 + '@tryghost/errors': + specifier: ^1.3.7 + version: 1.3.13 + '@tryghost/promise': + specifier: 2.2.3 + version: 2.2.3 + '@tryghost/tpl': + specifier: 2.2.3 + version: 2.2.3 + '@tryghost/validator': + specifier: 0.2.22 + version: 0.2.22 + lodash: + specifier: 'catalog:' + version: 4.18.1 + devDependencies: + sinon: + specifier: 'catalog:' + version: 22.0.0 + vitest: + specifier: 'catalog:' + version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@25.9.1)(typescript@5.9.3))(vite@7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) + ghost/core: dependencies: '@aws-sdk/client-s3': @@ -2339,8 +2364,8 @@ importers: specifier: 4.7.5 version: 4.7.5 '@tryghost/api-framework': - specifier: 'catalog:' - version: 3.2.4 + specifier: workspace:* + version: link:../api-framework '@tryghost/bookshelf-plugins': specifier: 2.2.4 version: 2.2.4 @@ -8694,9 +8719,6 @@ packages: '@tryghost/admin-api-schema@4.7.5': resolution: {integrity: sha512-k8XWP5g6hwpe3DcxhujBVHrDSkUSZ1nedR/TBony3TMbqNCMi4v40HkOhXiryAZ/ENDcmlKj/ZLzYDgG6cb0ww==} - '@tryghost/api-framework@3.2.4': - resolution: {integrity: sha512-Otz6uxGsedTsDqQIw62sQ8pmDpiaLMyvsSvCWUBUc44eMix5LFjZ1FMxKl2jXettj6tfMa04+Qz0p71zW/g/Qg==} - '@tryghost/bookshelf-collision@2.2.4': resolution: {integrity: sha512-2SV1QZkvXiYBXEWSvCeDc5YuHM7fRRcT2N8z/RHpOmDYyWHslmrdKuWb+jJOhIeRmO+oH0pYC4Nk9haQI2dgIg==} @@ -8995,9 +9017,6 @@ packages: '@tryghost/validator@3.1.1': resolution: {integrity: sha512-GIBONNQs7BP7FEKf8XxDk+roLVf86rQle3YWbe4IajqAcHoto952RAARwSAvYOhmqroBfX2++9BAyXEER+Ys9Q==} - '@tryghost/validator@3.1.4': - resolution: {integrity: sha512-xHLD9iek3zeINSy0ZYjVbzQMDgCSGEIzcuT1WUR9+zSBOmQzAXMfvykfn5ppglMroIOUJ4V9OIWcimybQ57BSQ==} - '@tryghost/version@0.1.38': resolution: {integrity: sha512-kVaM7eijFRrBLZLDbmiFxHQ/shmAg5UQ9yM4s9GZN4M3OWY34UHNmjoSOniOHnA5E8fYNA8A3IFmS3FW910xYg==} @@ -14772,7 +14791,7 @@ packages: csstype: ^3.0.10 google-caja-bower@https://codeload.github.com/acburdine/google-caja-bower/tar.gz/275cb75249f038492094a499756a73719ae071fd: - resolution: {gitHosted: true, tarball: https://codeload.github.com/acburdine/google-caja-bower/tar.gz/275cb75249f038492094a499756a73719ae071fd} + resolution: {gitHosted: true, integrity: sha512-mmCXdxGKGKDznjgkNzVqzTslaldslk5KMb/A7l8rxWnqyxzwsdPhuBJ6oT1Kh/Y3k4jN54ISee/2AgjFyCBxYw==, tarball: https://codeload.github.com/acburdine/google-caja-bower/tar.gz/275cb75249f038492094a499756a73719ae071fd} version: 6011.0.0 gopd@1.2.0: @@ -16165,7 +16184,7 @@ packages: engines: {node: '>= 0.6'} keymaster@https://codeload.github.com/madrobby/keymaster/tar.gz/f8f43ddafad663b505dc0908e72853bcf8daea49: - resolution: {gitHosted: true, tarball: https://codeload.github.com/madrobby/keymaster/tar.gz/f8f43ddafad663b505dc0908e72853bcf8daea49} + resolution: {gitHosted: true, integrity: sha512-/WVovQslVEqPGNoD97TbqNHuCDPYu2v4/ggrZj0a+9PVPw3Rud4Ut2K7fOi0kMqzoJINkgP68e9m09Al/wFZ8g==, tarball: https://codeload.github.com/madrobby/keymaster/tar.gz/f8f43ddafad663b505dc0908e72853bcf8daea49} version: 1.6.3 keypair@1.0.4: @@ -28478,17 +28497,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@tryghost/api-framework@3.2.4': - dependencies: - '@tryghost/debug': 2.2.3 - '@tryghost/errors': 1.3.13 - '@tryghost/promise': 2.2.3 - '@tryghost/tpl': 2.2.3 - '@tryghost/validator': 3.1.4 - lodash: 4.18.1 - transitivePeerDependencies: - - supports-color - '@tryghost/bookshelf-collision@2.2.4': dependencies: '@tryghost/errors': 1.3.13 @@ -29175,16 +29183,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@tryghost/validator@3.1.4': - dependencies: - '@tryghost/errors': 1.3.13 - '@tryghost/tpl': 2.2.3 - lodash: 4.18.1 - moment-timezone: 0.5.45 - validator: 13.15.35 - transitivePeerDependencies: - - supports-color - '@tryghost/version@0.1.38': dependencies: '@tryghost/root-utils': 0.3.38 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 74986f872ab..582ad0d73df 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -47,7 +47,6 @@ catalog: '@tanstack/react-query': 4.44.0 '@testing-library/jest-dom': 6.9.1 '@testing-library/react': 14.3.1 - '@tryghost/api-framework': 3.2.4 '@tryghost/color-utils': 0.2.19 '@tryghost/custom-fonts': 1.0.11 '@tryghost/debug': 2.2.3 diff --git a/vitest.config.mjs b/vitest.config.mjs index ff2bb0f1ec2..168647b065b 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -14,6 +14,7 @@ export default defineConfig({ test: { projects: [ 'ghost/core', + 'ghost/api-framework', 'apps/*', '!apps/admin-x-activitypub', '!apps/signup-form' From 2e48e472e92fc9cb286b052e7926878e2a532dc7 Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Thu, 18 Jun 2026 08:32:23 +0000 Subject: [PATCH 118/118] Added oxlint + oxfmt lint setup for api-framework - the package was linted with oxlint/oxfmt in the framework repo; keeping that toolchain self-contained (local .oxlintrc.json/.oxfmtrc.json + catalog'd devDeps) preserves its lint coverage without pulling the code through Ghost's eslint rules - the lint target runs oxlint so Nx run-many -t lint discovers it; format/format:check use oxfmt scoped to js/ts/md so package.json and JSON configs keep Ghost's 2-space manifest style --- ghost/api-framework/.oxfmtrc.json | 13 + ghost/api-framework/.oxlintrc.json | 12 + ghost/api-framework/package.json | 7 +- pnpm-lock.yaml | 448 +++++++++++++++++++++++++++++ pnpm-workspace.yaml | 2 + 5 files changed, 481 insertions(+), 1 deletion(-) create mode 100644 ghost/api-framework/.oxfmtrc.json create mode 100644 ghost/api-framework/.oxlintrc.json diff --git a/ghost/api-framework/.oxfmtrc.json b/ghost/api-framework/.oxfmtrc.json new file mode 100644 index 00000000000..99c61b470e8 --- /dev/null +++ b/ghost/api-framework/.oxfmtrc.json @@ -0,0 +1,13 @@ +{ + "singleQuote": true, + "ignorePatterns": [ + "**/node_modules/**", + "**/build/**", + "**/coverage/**", + "**/cjs/**", + "**/es/**", + "**/types/**", + "**/*.hbs", + "**/test/fixtures/**" + ] +} diff --git a/ghost/api-framework/.oxlintrc.json b/ghost/api-framework/.oxlintrc.json new file mode 100644 index 00000000000..1fb62e042d9 --- /dev/null +++ b/ghost/api-framework/.oxlintrc.json @@ -0,0 +1,12 @@ +{ + "plugins": ["node", "typescript"], + "ignorePatterns": [ + "**/node_modules/**", + "**/build/**", + "**/coverage/**", + "**/cjs/**", + "**/es/**", + "**/types/**", + "**/test/fixtures/**" + ] +} diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index fa64045e20e..cc428dd525c 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -7,7 +7,10 @@ "scripts": { "dev": "echo \"Implement me!\"", "test:unit": "NODE_ENV=testing vitest run", - "test": "pnpm test:unit" + "test": "pnpm test:unit", + "lint": "oxlint -c .oxlintrc.json .", + "format": "oxfmt -c .oxfmtrc.json \"**/*.{js,ts,md}\"", + "format:check": "oxfmt -c .oxfmtrc.json --check \"**/*.{js,ts,md}\"" }, "dependencies": { "@tryghost/debug": "catalog:", @@ -18,6 +21,8 @@ "lodash": "catalog:" }, "devDependencies": { + "oxfmt": "catalog:", + "oxlint": "catalog:", "sinon": "catalog:", "vitest": "catalog:" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 21bcef93d8e..b6eff0ac693 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -219,6 +219,12 @@ catalogs: node-html-markdown: specifier: 2.0.0 version: 2.0.0 + oxfmt: + specifier: 0.54.0 + version: 0.54.0 + oxlint: + specifier: 1.69.0 + version: 1.69.0 postcss: specifier: 8.5.15 version: 8.5.15 @@ -2330,6 +2336,12 @@ importers: specifier: 'catalog:' version: 4.18.1 devDependencies: + oxfmt: + specifier: 'catalog:' + version: 0.54.0 + oxlint: + specifier: 'catalog:' + version: 1.69.0 sinon: specifier: 'catalog:' version: 22.0.0 @@ -6277,6 +6289,250 @@ packages: cpu: [x64] os: [win32] + '@oxfmt/binding-android-arm-eabi@0.54.0': + resolution: {integrity: sha512-NAtpl/SiaeU103e7/OmZw0MvUnsUUopW7hEm/ecegJg7YM0skQaA0IXEZoyTV6NUdiNPupdIUreRqUZTShbn/g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxfmt/binding-android-arm64@0.54.0': + resolution: {integrity: sha512-B4VZfBUlKK1rmMChsssNZbkZjE8+FzG3avMjGgMDwbGxXRoXkoeXiAZ+78Oa+eyDPHvDCiUb4zH/vmCOUSafLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxfmt/binding-darwin-arm64@0.54.0': + resolution: {integrity: sha512-i02vF75b+ePsQP3tHqSxVYI5S6b8X/xqdPu7/mDHXtpgXLTYXi3jJmfHU0j+dnZZDKaYTx/ioCK7QYJmtiJR2g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxfmt/binding-darwin-x64@0.54.0': + resolution: {integrity: sha512-8VMFvGvooXj7mswkbrhdVZ2/sgiDaBzWpkkbtO+qGDLV4EfJd67nQadHkQC0ZNbaWA9ajXfqI6i7PZLIeDzxEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxfmt/binding-freebsd-x64@0.54.0': + resolution: {integrity: sha512-0cRHnp43WN1Jrc5s0BdbdKgR1XirdvHy7TAFi3JEsoEVQVJxTXMbpVd76sxXlgRswNMDhVFSJw+y7Eb8mEavFQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxfmt/binding-linux-arm-gnueabihf@0.54.0': + resolution: {integrity: sha512-JyQAk3hK/OEtup7Rw6kZwfdzbKqTVD5jXXb8Xpfay29suwZyfBDMVW/bj4RqEPySYWc6zCp198pOluf8n5uYzg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.54.0': + resolution: {integrity: sha512-qnvLatTpM8vtvjOfcckBOzJjk+n6ce/wwpP8OFeUrD5aNLYcKyWAitwj+Rk3PK9jGanbZvKsJnv14JGQ6XqFdw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm64-gnu@0.54.0': + resolution: {integrity: sha512-SMkhnCzIYZYDk9vw3W/80eeYKmrMpGF0Giuxt4HruFlCH7jEtnPeb3SdQKMfgYi/dgtaf+hZAb5XWPYnxqCQ3w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-arm64-musl@0.54.0': + resolution: {integrity: sha512-QrwJlBFFKnxOd95TAaszpMbZBLzMoYMpGaQTZF8oibacnF5rv8l12IhILhQRPmksWiBqg0YSe2Mnl7ayeJAHSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-ppc64-gnu@0.54.0': + resolution: {integrity: sha512-WILatiol/TUHTlhod7R09+7Az/XlhKwmY1MHfLZNmewltPWNN/EwxP2rQSHahibZ/cB8gmckEBjBOByD+5bYsQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-gnu@0.54.0': + resolution: {integrity: sha512-f05YMG4BH4G8S4ME6UM6fi1MnJ9094mrnvO5Pa4SJlMfWlUM+1/ZWMEF4NnjM7shZAvbHsHRuVYpUo0PHC4P9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-musl@0.54.0': + resolution: {integrity: sha512-UfL+2hj1ClNqcCRT9s8vBU4axDpjxgVxX96G+9DYAYjoc5b0u15CJtn2jgsi9iM+EbGNc5CW1HVRgwVu76UsSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-s390x-gnu@0.54.0': + resolution: {integrity: sha512-3/XZe931Hka+J6NjnaqJzYpsWWxDTuRdUdwSQHnOuJEgbC+SehIMFJS8hsEjV7LBhVSL2OCnRLvbVW8O97XIyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-gnu@0.54.0': + resolution: {integrity: sha512-Ik93RlObtu43GbxApafayFjwYE06L6Xr08cSwpBPYbDrLp2ReZx0Jm1DqwRyYRnukUJy+rK2WaEvUQOxdytU9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-musl@0.54.0': + resolution: {integrity: sha512-yZcakmPlD86CNymknd7KfW+FH+qfbqJH+i0h69CYfV1+KMoVeM9UED+8+TDVoU4haxI0NxY7RPCvRLy3Sqd2Qg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-openharmony-arm64@0.54.0': + resolution: {integrity: sha512-GiVBZNnEZnKu00f1jTg49nomv187d0GQX+O+ocykoLeiaALuEO+swoTehHn9TehTfi7V8H0i0e/yvUjCqnwk1w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxfmt/binding-win32-arm64-msvc@0.54.0': + resolution: {integrity: sha512-J0SSB8Z1Fre2sxRolYcW6Rl1RQmKdQ2hnHyq4YJrfBRiXTObLw4DXnIVraM/UyqGqwOi7yTrQA4VT7DPxlHVKA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxfmt/binding-win32-ia32-msvc@0.54.0': + resolution: {integrity: sha512-O61UDVj8zz6yXJjkHPf05VaMLOXmEF8P5kf/N0W7AQMmd6bcQogl+KJc7rMutKTL524oE9iH32JXZClBFmEQIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxfmt/binding-win32-x64-msvc@0.54.0': + resolution: {integrity: sha512-1MDpqJPiFqxWtIHas8vkb1VZ7f7eKyTffAwmO8isxQYMaG1OFKsH666BWLeXQLO+IWNfiMssLD55hbR1lIPTqg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint/binding-android-arm-eabi@1.69.0': + resolution: {integrity: sha512-DKQQbD5cZ/MYfDgDI7YGyGD9FSxABlsBsYFo5p26lloob543tP9+4N3guwdXIYJN+7HSZxLe8YJuwcOWw5qnHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.69.0': + resolution: {integrity: sha512-lEhb+I5pr4inux+JFwfCa1HRq3Os7NirEFQ0H1I35SVEHPm6byX0Ah47xmRha3qi6LAkxUcxViL8o/9PivjzBg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.69.0': + resolution: {integrity: sha512-GY2YE8lOZW59BW1Ia1y+1gR0XyjrZRvVWHAr8LGeGhYHE0OQJ/7cRKXTkx1P+E9/6awEc3SX8a68SFTjh/E//A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.69.0': + resolution: {integrity: sha512-ax1oZnOjHX3LB7myQyHEaQkDwfLb6str3/nSP6O7EVUviQGNkEGzGV0EqcBJWK+Ufwx0l4xPgyYayurvhAdl2Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.69.0': + resolution: {integrity: sha512-kHWeHv4g2h8NY+mpCxzCtY4uerMJWTN/TSnNj1CPbakFpHEJ6cTya2wWV0pDSYWOJ2+0UiEbhn3AtXxHtsnKjg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.69.0': + resolution: {integrity: sha512-gq84vM1a1oEehXo27YCDzGVcxPsZDI1yswZwz2Da1/cbnWtrL16XZZnz0G/+gIU8edtHpfjxq5c+vWEHqJfWoQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.69.0': + resolution: {integrity: sha512-kIqEa98JQ0VRyrcncxA417m2AzasqTlD+FyVT1AksjvjkqQcvm7pBWYvoW3/mpyOP2XYvi5nSCCTIe6De1yu5g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.69.0': + resolution: {integrity: sha512-j+xYiXozxGWx2cpjCrwwGR4awTxPFsRv3JZrv23RCogEPMc4R7UqjHW47p/RG0aRlbWiROCJ8coUfCwy0dvzHA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.69.0': + resolution: {integrity: sha512-xEPpNppTfN1l/nM7gYSf9iocscu/as+p/7vxkLeLEKnYU+09Dm+5V6IhDYDh+Uz6FajEupWwCLt5SOG0y1PCKg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.69.0': + resolution: {integrity: sha512-Ug0+eU7HJBlek+SjklYH62IlOMirEJsdxpihH0kSqX0XdrDD4NdHpQc10fK1JC35yn6KrrcN+uYzlHD38XAf8Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.69.0': + resolution: {integrity: sha512-iEyI3GIg0l/s3G4qy2TlaaWKdzj4PJJStwtlocpDTC00PY9hZueotf6OKUj9+yfQh0lrpBW/pLMgTztbAHKJEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.69.0': + resolution: {integrity: sha512-NjHjpiI4WIKSMwuoJSZi5VToPeoYOS1FR52HLIDG6lidMdqquusgtODb4iLk0+lb1q3Z0nv2/aPRcC/olmpQGg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.69.0': + resolution: {integrity: sha512-Ai/prDewoItkDXbp38gwGZi41DycZbUTZJ3UidwoHgQC0/DaqC2TGdtBTQLJ6hSD+SAxASzh8+/eSBPmxfOacA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.69.0': + resolution: {integrity: sha512-Gt3KHgp46mRKz4sJeaASmKvD8ayXookRw07RMf+NowhEztGGDZ7VrXpoW96XuKJLjFukWizOFVNjmYb/u7caNQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.69.0': + resolution: {integrity: sha512-7tQhJ2+p/oHv1zcfnjYI7YVzC/7iBaVOfIvFYtxdJ5F45mWgEdrCyXZXZGfiLey5t/5JhOhsaMnnv1kAzckd7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.69.0': + resolution: {integrity: sha512-vmWz6TKp/3hfA4lksR0zHBv/6xuX1jhym6eqOjdH2DXsDDHZWcp2f0KG0VCAnlVbIrjk29G4wAWMXb/Hn1YobA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.69.0': + resolution: {integrity: sha512-9RExaLgmaw6IoIkU9cTpT71mLfI0xZ86iZH8x518LVsOkjquJMYqb9P7KpC8lgd1t0Dxs41p2pxynq4XR3Ttzw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.69.0': + resolution: {integrity: sha512-1907kRPF8/PrcIw1E7LMs9JbVrpgnt/MvFdss3an8oDkYNAACXzTntV3t3869ZZhMZxb2AzRGbz1pA/jdFatXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.69.0': + resolution: {integrity: sha512-w8SOXv3mT9Fi6jY8OXdXCfnvX/3KNLXGNr4HEz2TA7S4Mv/PYAOmpB8y/ge40mxvBMgGNaSaaDwZpAsQn7HtWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} @@ -17925,6 +18181,32 @@ packages: oxc-resolver@11.20.0: resolution: {integrity: sha512-CblytBiV/a/ZXY34dsVU2NxhIOxMXst8CvDCtyBelVITgd7PLrKzbEbA6oKLdPjvDKDzCiW48qzmzZ+mYaqn+g==} + oxfmt@0.54.0: + resolution: {integrity: sha512-DjnMwn7smSLF+Mc2+pRItnuPftm/dkUFpY/d4+33y9TfKrsHZo8GLhmUg9BrOIUEy94Rlom1Q11N6vuhE+e0oQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + svelte: ^5.0.0 + vite-plus: '*' + peerDependenciesMeta: + svelte: + optional: true + vite-plus: + optional: true + + oxlint@1.69.0: + resolution: {integrity: sha512-ypZkK/aDc5NQV8zIR6s2H2Tl3aNW8FmJ1m9+2qsaYuRenl8vgnHNCGwTHviWJdUQzglOlHFchgopdtGhSy17Rw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.22.1' + vite-plus: '*' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true + p-cancelable@2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} @@ -20927,6 +21209,10 @@ packages: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} + tinypool@2.1.0: + resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} + engines: {node: ^20.0.0 || >=22.0.0} + tinyrainbow@2.0.0: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} @@ -25838,6 +26124,120 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.20.0': optional: true + '@oxfmt/binding-android-arm-eabi@0.54.0': + optional: true + + '@oxfmt/binding-android-arm64@0.54.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.54.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.54.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.54.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.54.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.54.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.54.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.54.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.54.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.54.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.54.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.54.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.54.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.54.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.54.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.54.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.54.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.54.0': + optional: true + + '@oxlint/binding-android-arm-eabi@1.69.0': + optional: true + + '@oxlint/binding-android-arm64@1.69.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.69.0': + optional: true + + '@oxlint/binding-darwin-x64@1.69.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.69.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.69.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.69.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.69.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.69.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.69.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.69.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.69.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.69.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.69.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.69.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.69.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.69.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.69.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.69.0': + optional: true + '@paralleldrive/cuid2@2.3.1': dependencies: '@noble/hashes': 1.8.0 @@ -41850,6 +42250,52 @@ snapshots: '@oxc-resolver/binding-win32-arm64-msvc': 11.20.0 '@oxc-resolver/binding-win32-x64-msvc': 11.20.0 + oxfmt@0.54.0: + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.54.0 + '@oxfmt/binding-android-arm64': 0.54.0 + '@oxfmt/binding-darwin-arm64': 0.54.0 + '@oxfmt/binding-darwin-x64': 0.54.0 + '@oxfmt/binding-freebsd-x64': 0.54.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.54.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.54.0 + '@oxfmt/binding-linux-arm64-gnu': 0.54.0 + '@oxfmt/binding-linux-arm64-musl': 0.54.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.54.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.54.0 + '@oxfmt/binding-linux-riscv64-musl': 0.54.0 + '@oxfmt/binding-linux-s390x-gnu': 0.54.0 + '@oxfmt/binding-linux-x64-gnu': 0.54.0 + '@oxfmt/binding-linux-x64-musl': 0.54.0 + '@oxfmt/binding-openharmony-arm64': 0.54.0 + '@oxfmt/binding-win32-arm64-msvc': 0.54.0 + '@oxfmt/binding-win32-ia32-msvc': 0.54.0 + '@oxfmt/binding-win32-x64-msvc': 0.54.0 + + oxlint@1.69.0: + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.69.0 + '@oxlint/binding-android-arm64': 1.69.0 + '@oxlint/binding-darwin-arm64': 1.69.0 + '@oxlint/binding-darwin-x64': 1.69.0 + '@oxlint/binding-freebsd-x64': 1.69.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.69.0 + '@oxlint/binding-linux-arm-musleabihf': 1.69.0 + '@oxlint/binding-linux-arm64-gnu': 1.69.0 + '@oxlint/binding-linux-arm64-musl': 1.69.0 + '@oxlint/binding-linux-ppc64-gnu': 1.69.0 + '@oxlint/binding-linux-riscv64-gnu': 1.69.0 + '@oxlint/binding-linux-riscv64-musl': 1.69.0 + '@oxlint/binding-linux-s390x-gnu': 1.69.0 + '@oxlint/binding-linux-x64-gnu': 1.69.0 + '@oxlint/binding-linux-x64-musl': 1.69.0 + '@oxlint/binding-openharmony-arm64': 1.69.0 + '@oxlint/binding-win32-arm64-msvc': 1.69.0 + '@oxlint/binding-win32-ia32-msvc': 1.69.0 + '@oxlint/binding-win32-x64-msvc': 1.69.0 + p-cancelable@2.1.1: {} p-cancelable@3.0.0: {} @@ -45497,6 +45943,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinypool@2.1.0: {} + tinyrainbow@2.0.0: {} tinyrainbow@3.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 582ad0d73df..10858a61f36 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -97,6 +97,8 @@ catalog: mocha: 11.7.6 msw: 2.14.6 node-html-markdown: 2.0.0 + oxfmt: 0.54.0 + oxlint: 1.69.0 postcss: 8.5.15 preact: ^10.29.2 react: 18.3.1