Added endpoint for fixing newsletter links
refs https://github.com/TryGhost/Team/issues/2104 - adds new bulk edit endpoint for links, updates all matching link with the current redirect url and update to new url
This commit is contained in:
parent
40b8a838ae
commit
d8bacf12d1
@ -21,5 +21,35 @@ module.exports = {
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
bulkEdit: {
|
||||
statusCode: 200,
|
||||
headers: {},
|
||||
options: [
|
||||
'filter'
|
||||
],
|
||||
data: [
|
||||
'action',
|
||||
'meta'
|
||||
],
|
||||
validation: {
|
||||
data: {
|
||||
action: {
|
||||
required: true,
|
||||
values: ['updateLink']
|
||||
}
|
||||
},
|
||||
options: {
|
||||
filter: {
|
||||
required: true
|
||||
}
|
||||
}
|
||||
},
|
||||
permissions: {
|
||||
method: 'edit'
|
||||
},
|
||||
async query(frame) {
|
||||
return await linkTrackingService.service.bulkEdit(frame.data.bulk, frame.options);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -127,5 +127,9 @@ module.exports = {
|
||||
|
||||
get members_stripe_connect() {
|
||||
return require('./members-stripe-connect');
|
||||
},
|
||||
|
||||
get links() {
|
||||
return require('./links');
|
||||
}
|
||||
};
|
||||
|
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
bulkEdit(data, apiConfig, frame) {
|
||||
frame.response = data;
|
||||
}
|
||||
};
|
@ -18,7 +18,7 @@ module.exports = class LinkRedirectRepository {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {InstanceType<LinkRedirect>} linkRedirect
|
||||
* @param {InstanceType<LinkRedirect>} linkRedirect
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async save(linkRedirect) {
|
||||
@ -55,10 +55,17 @@ module.exports = class LinkRedirectRepository {
|
||||
return result;
|
||||
}
|
||||
|
||||
async getFilteredIds(options) {
|
||||
const linkRows = await this.#LinkRedirect.getFilteredCollectionQuery(options)
|
||||
.select('redirects.id')
|
||||
.distinct();
|
||||
return linkRows.map(row => row.id);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {URL} url
|
||||
* @returns {Promise<InstanceType<LinkRedirect>|undefined>} linkRedirect
|
||||
*
|
||||
* @param {URL} url
|
||||
* @returns {Promise<InstanceType<LinkRedirect>|undefined>} linkRedirect
|
||||
*/
|
||||
async getByURL(url) {
|
||||
// Strip subdirectory from path
|
||||
|
@ -1,4 +1,5 @@
|
||||
const {FullPostLink} = require('@tryghost/link-tracking');
|
||||
const _ = require('lodash');
|
||||
|
||||
/**
|
||||
* @typedef {import('bson-objectid').default} ObjectID
|
||||
@ -22,8 +23,8 @@ module.exports = class PostLinkRepository {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} options
|
||||
*
|
||||
* @param {*} options
|
||||
* @returns {Promise<InstanceType<FullPostLink>[]>}
|
||||
*/
|
||||
async getAll(options) {
|
||||
@ -48,6 +49,29 @@ module.exports = class PostLinkRepository {
|
||||
return result;
|
||||
}
|
||||
|
||||
async updateLinks(linkIds, updateData, options) {
|
||||
const bulkUpdateOptions = _.pick(options, ['transacting']);
|
||||
|
||||
const bulkActionResult = await this.#LinkRedirect.bulkEdit(linkIds, 'redirects', {
|
||||
...bulkUpdateOptions,
|
||||
data: updateData
|
||||
});
|
||||
|
||||
return {
|
||||
bulk: {
|
||||
action: 'updateLink',
|
||||
meta: {
|
||||
stats: {
|
||||
successful: bulkActionResult.successful,
|
||||
unsuccessful: bulkActionResult.unsuccessful
|
||||
},
|
||||
errors: bulkActionResult.errors,
|
||||
unsuccessfulData: bulkActionResult.unsuccessfulData
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PostLink} postLink
|
||||
* @returns {Promise<void>}
|
||||
|
@ -1,6 +1,7 @@
|
||||
const LinkClickRepository = require('./LinkClickRepository');
|
||||
const PostLinkRepository = require('./PostLinkRepository');
|
||||
const errors = require('@tryghost/errors');
|
||||
const urlUtils = require('../../../shared/url-utils');
|
||||
|
||||
class LinkTrackingServiceWrapper {
|
||||
async init() {
|
||||
@ -38,7 +39,8 @@ class LinkTrackingServiceWrapper {
|
||||
linkRedirectService: linkRedirection.service,
|
||||
linkClickRepository: this.linkClickRepository,
|
||||
postLinkRepository,
|
||||
DomainEvents
|
||||
DomainEvents,
|
||||
urlUtils
|
||||
});
|
||||
|
||||
await this.service.init();
|
||||
|
@ -310,6 +310,7 @@ module.exports = function apiRoutes() {
|
||||
router.put('/newsletters/:id', mw.authAdminApi, http(api.newsletters.edit));
|
||||
|
||||
router.get('/links', mw.authAdminApi, http(api.links.browse));
|
||||
router.put('/links/bulk', mw.authAdminApi, http(api.links.bulkEdit));
|
||||
|
||||
return router;
|
||||
};
|
||||
|
557
ghost/core/test/e2e-api/admin/__snapshots__/links.test.js.snap
Normal file
557
ghost/core/test/e2e-api/admin/__snapshots__/links.test.js.snap
Normal file
@ -0,0 +1,557 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Links API Can browse all links 1: [body] 1`] = `
|
||||
Object {
|
||||
"links": Array [
|
||||
Object {
|
||||
"count": Object {
|
||||
"clicks": Any<Number>,
|
||||
},
|
||||
"link": Object {
|
||||
"from": Any<String>,
|
||||
"link_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"to": Any<String>,
|
||||
},
|
||||
"post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
},
|
||||
Object {
|
||||
"count": Object {
|
||||
"clicks": Any<Number>,
|
||||
},
|
||||
"link": Object {
|
||||
"from": Any<String>,
|
||||
"link_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"to": Any<String>,
|
||||
},
|
||||
"post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
},
|
||||
Object {
|
||||
"count": Object {
|
||||
"clicks": Any<Number>,
|
||||
},
|
||||
"link": Object {
|
||||
"from": Any<String>,
|
||||
"link_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"to": Any<String>,
|
||||
},
|
||||
"post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
},
|
||||
],
|
||||
"meta": Object {
|
||||
"pagination": Object {
|
||||
"page": 1,
|
||||
"pages": 1,
|
||||
"total": 3,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Links API Can browse all links 2: [body] 1`] = `
|
||||
Object {
|
||||
"links": Array [
|
||||
Object {
|
||||
"count": Object {
|
||||
"clicks": Any<Number>,
|
||||
},
|
||||
"link": Object {
|
||||
"from": Any<String>,
|
||||
"link_id": Any<String>,
|
||||
"to": Any<String>,
|
||||
},
|
||||
"post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
},
|
||||
Object {
|
||||
"count": Object {
|
||||
"clicks": Any<Number>,
|
||||
},
|
||||
"link": Object {
|
||||
"from": Any<String>,
|
||||
"link_id": Any<String>,
|
||||
"to": Any<String>,
|
||||
},
|
||||
"post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
},
|
||||
Object {
|
||||
"count": Object {
|
||||
"clicks": Any<Number>,
|
||||
},
|
||||
"link": Object {
|
||||
"from": Any<String>,
|
||||
"link_id": Any<String>,
|
||||
"to": Any<String>,
|
||||
},
|
||||
"post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
},
|
||||
],
|
||||
"meta": Object {
|
||||
"pagination": Object {
|
||||
"page": 1,
|
||||
"pages": 1,
|
||||
"total": 3,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Links API Can browse all links 2: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-length": "885",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Links API Can browse all links 3: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-length": "885",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Links API Can bulk update links with external redirect 1: [body] 1`] = `
|
||||
Object {
|
||||
"bulk": Object {
|
||||
"action": "updateLink",
|
||||
"meta": Object {
|
||||
"errors": Array [],
|
||||
"stats": Object {
|
||||
"successful": 1,
|
||||
"unsuccessful": 0,
|
||||
},
|
||||
"unsuccessfulData": Array [],
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Links API Can bulk update links with external redirect 2: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-length": "117",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Links API Can bulk update links with external redirect 3: [body] 1`] = `
|
||||
Object {
|
||||
"links": Array [
|
||||
Object {
|
||||
"count": Object {
|
||||
"clicks": Any<Number>,
|
||||
},
|
||||
"link": Object {
|
||||
"from": Any<String>,
|
||||
"link_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"to": "https://example.com/subscribe?ref=Test-newsletter",
|
||||
},
|
||||
"post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
},
|
||||
Object {
|
||||
"count": Object {
|
||||
"clicks": Any<Number>,
|
||||
},
|
||||
"link": Object {
|
||||
"from": Any<String>,
|
||||
"link_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"to": Any<String>,
|
||||
},
|
||||
"post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
},
|
||||
Object {
|
||||
"count": Object {
|
||||
"clicks": Any<Number>,
|
||||
},
|
||||
"link": Object {
|
||||
"from": Any<String>,
|
||||
"link_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"to": Any<String>,
|
||||
},
|
||||
"post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
},
|
||||
],
|
||||
"meta": Object {
|
||||
"pagination": Object {
|
||||
"page": 1,
|
||||
"pages": 1,
|
||||
"total": 3,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Links API Can bulk update links with external redirect 4: [body] 1`] = `
|
||||
Object {
|
||||
"links": Array [
|
||||
Object {
|
||||
"count": Object {
|
||||
"clicks": Any<Number>,
|
||||
},
|
||||
"link": Object {
|
||||
"from": Any<String>,
|
||||
"link_id": Any<String>,
|
||||
"to": "https://example.com/subscribe?ref=Test-newsletter",
|
||||
},
|
||||
"post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
},
|
||||
Object {
|
||||
"count": Object {
|
||||
"clicks": Any<Number>,
|
||||
},
|
||||
"link": Object {
|
||||
"from": Any<String>,
|
||||
"link_id": Any<String>,
|
||||
"to": Any<String>,
|
||||
},
|
||||
"post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
},
|
||||
Object {
|
||||
"count": Object {
|
||||
"clicks": Any<Number>,
|
||||
},
|
||||
"link": Object {
|
||||
"from": Any<String>,
|
||||
"link_id": Any<String>,
|
||||
"to": Any<String>,
|
||||
},
|
||||
"post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
},
|
||||
],
|
||||
"meta": Object {
|
||||
"pagination": Object {
|
||||
"page": 1,
|
||||
"pages": 1,
|
||||
"total": 3,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Links API Can bulk update links with external redirect 4: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-length": "885",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Links API Can bulk update links with external redirect 5: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-length": "885",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Links API Can bulk update multiple links with same site redirect 1: [body] 1`] = `
|
||||
Object {
|
||||
"bulk": Object {
|
||||
"action": "updateLink",
|
||||
"meta": Object {
|
||||
"errors": Array [],
|
||||
"stats": Object {
|
||||
"successful": 2,
|
||||
"unsuccessful": 0,
|
||||
},
|
||||
"unsuccessfulData": Array [],
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Links API Can bulk update multiple links with same site redirect 2: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-length": "117",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Links API Can bulk update multiple links with same site redirect 3: [body] 1`] = `
|
||||
Object {
|
||||
"links": Array [
|
||||
Object {
|
||||
"count": Object {
|
||||
"clicks": Any<Number>,
|
||||
},
|
||||
"link": Object {
|
||||
"from": Any<String>,
|
||||
"link_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"to": Any<String>,
|
||||
},
|
||||
"post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
},
|
||||
Object {
|
||||
"count": Object {
|
||||
"clicks": Any<Number>,
|
||||
},
|
||||
"link": Object {
|
||||
"from": Any<String>,
|
||||
"link_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"to": "http://127.0.0.1:2369/blog/emails/test?example=1&ref=Test-newsletter&attribution_type=post&attribution_id=618ba1ffbe2896088840a6df",
|
||||
},
|
||||
"post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
},
|
||||
Object {
|
||||
"count": Object {
|
||||
"clicks": Any<Number>,
|
||||
},
|
||||
"link": Object {
|
||||
"from": Any<String>,
|
||||
"link_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"to": "http://127.0.0.1:2369/blog/emails/test?example=1&ref=Test-newsletter&attribution_type=post&attribution_id=618ba1ffbe2896088840a6df",
|
||||
},
|
||||
"post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
},
|
||||
],
|
||||
"meta": Object {
|
||||
"pagination": Object {
|
||||
"page": 1,
|
||||
"pages": 1,
|
||||
"total": 3,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Links API Can bulk update multiple links with same site redirect 4: [body] 1`] = `
|
||||
Object {
|
||||
"links": Array [
|
||||
Object {
|
||||
"count": Object {
|
||||
"clicks": Any<Number>,
|
||||
},
|
||||
"link": Object {
|
||||
"from": Any<String>,
|
||||
"link_id": Any<String>,
|
||||
"to": Any<String>,
|
||||
},
|
||||
"post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
},
|
||||
Object {
|
||||
"count": Object {
|
||||
"clicks": Any<Number>,
|
||||
},
|
||||
"link": Object {
|
||||
"from": Any<String>,
|
||||
"link_id": Any<String>,
|
||||
"to": "http://127.0.0.1:2369/blog/emails/test?example=1&ref=Test-newsletter&attribution_type=post&attribution_id=618ba1ffbe2896088840a6df",
|
||||
},
|
||||
"post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
},
|
||||
Object {
|
||||
"count": Object {
|
||||
"clicks": Any<Number>,
|
||||
},
|
||||
"link": Object {
|
||||
"from": Any<String>,
|
||||
"link_id": Any<String>,
|
||||
"to": "http://127.0.0.1:2369/blog/emails/test?example=1&ref=Test-newsletter&attribution_type=post&attribution_id=618ba1ffbe2896088840a6df",
|
||||
},
|
||||
"post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
},
|
||||
],
|
||||
"meta": Object {
|
||||
"pagination": Object {
|
||||
"page": 1,
|
||||
"pages": 1,
|
||||
"total": 3,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Links API Can bulk update multiple links with same site redirect 4: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-length": "841",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Links API Can bulk update multiple links with same site redirect 5: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-length": "841",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Links API Can call bulk update link with 0 matches 1: [body] 1`] = `
|
||||
Object {
|
||||
"bulk": Object {
|
||||
"action": "updateLink",
|
||||
"meta": Object {
|
||||
"errors": Array [],
|
||||
"stats": Object {
|
||||
"successful": 0,
|
||||
"unsuccessful": 0,
|
||||
},
|
||||
"unsuccessfulData": Array [],
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Links API Can call bulk update link with 0 matches 2: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-length": "117",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Links API Can call bulk update link with 0 matches 3: [body] 1`] = `
|
||||
Object {
|
||||
"links": Array [
|
||||
Object {
|
||||
"count": Object {
|
||||
"clicks": Any<Number>,
|
||||
},
|
||||
"link": Object {
|
||||
"from": Any<String>,
|
||||
"link_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"to": "https://example.com/subscripe?ref=Test-newsletter",
|
||||
},
|
||||
"post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
},
|
||||
Object {
|
||||
"count": Object {
|
||||
"clicks": Any<Number>,
|
||||
},
|
||||
"link": Object {
|
||||
"from": Any<String>,
|
||||
"link_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"to": Any<String>,
|
||||
},
|
||||
"post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
},
|
||||
Object {
|
||||
"count": Object {
|
||||
"clicks": Any<Number>,
|
||||
},
|
||||
"link": Object {
|
||||
"from": Any<String>,
|
||||
"link_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"to": Any<String>,
|
||||
},
|
||||
"post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
},
|
||||
],
|
||||
"meta": Object {
|
||||
"pagination": Object {
|
||||
"page": 1,
|
||||
"pages": 1,
|
||||
"total": 3,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Links API Can call bulk update link with 0 matches 4: [body] 1`] = `
|
||||
Object {
|
||||
"links": Array [
|
||||
Object {
|
||||
"count": Object {
|
||||
"clicks": Any<Number>,
|
||||
},
|
||||
"link": Object {
|
||||
"from": Any<String>,
|
||||
"link_id": Any<String>,
|
||||
"to": "https://example.com/subscripe?ref=Test-newsletter",
|
||||
},
|
||||
"post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
},
|
||||
Object {
|
||||
"count": Object {
|
||||
"clicks": Any<Number>,
|
||||
},
|
||||
"link": Object {
|
||||
"from": Any<String>,
|
||||
"link_id": Any<String>,
|
||||
"to": Any<String>,
|
||||
},
|
||||
"post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
},
|
||||
Object {
|
||||
"count": Object {
|
||||
"clicks": Any<Number>,
|
||||
},
|
||||
"link": Object {
|
||||
"from": Any<String>,
|
||||
"link_id": Any<String>,
|
||||
"to": Any<String>,
|
||||
},
|
||||
"post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
},
|
||||
],
|
||||
"meta": Object {
|
||||
"pagination": Object {
|
||||
"page": 1,
|
||||
"pages": 1,
|
||||
"total": 3,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Links API Can call bulk update link with 0 matches 4: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-length": "885",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Links API Can call bulk update link with 0 matches 5: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-length": "885",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
215
ghost/core/test/e2e-api/admin/links.test.js
Normal file
215
ghost/core/test/e2e-api/admin/links.test.js
Normal file
@ -0,0 +1,215 @@
|
||||
const {agentProvider, fixtureManager, matchers} = require('../../utils/e2e-framework');
|
||||
const {anyObjectId, anyString, anyEtag, anyNumber} = matchers;
|
||||
|
||||
const matchLink = {
|
||||
post_id: anyObjectId,
|
||||
link: {
|
||||
link_id: anyObjectId,
|
||||
from: anyString,
|
||||
to: anyString
|
||||
},
|
||||
count: {
|
||||
clicks: anyNumber
|
||||
}
|
||||
};
|
||||
|
||||
describe('Links API', function () {
|
||||
let agent;
|
||||
beforeEach(async function () {
|
||||
agent = await agentProvider.getAdminAPIAgent();
|
||||
await fixtureManager.init('posts', 'links');
|
||||
await agent.loginAsOwner();
|
||||
});
|
||||
|
||||
it('Can browse all links', async function () {
|
||||
await agent
|
||||
.get('links')
|
||||
.expectStatus(200)
|
||||
.matchHeaderSnapshot({
|
||||
etag: anyEtag
|
||||
})
|
||||
.matchBodySnapshot({
|
||||
links: new Array(3).fill(matchLink)
|
||||
});
|
||||
});
|
||||
|
||||
it('Can bulk update multiple links with same site redirect', async function () {
|
||||
const req = await agent.get('links');
|
||||
const siteLink = req.body.links.find((link) => {
|
||||
return link.link.to.includes('/email/');
|
||||
});
|
||||
const postId = siteLink.post_id;
|
||||
const originalTo = siteLink.link.to;
|
||||
const filter = `post_id:${postId}+to:'${originalTo}'`;
|
||||
await agent
|
||||
.put(`links/bulk/?filter=${encodeURIComponent(filter)}`)
|
||||
.body({
|
||||
bulk: {
|
||||
action: 'updateLink',
|
||||
meta: {
|
||||
link: {
|
||||
to: 'http://127.0.0.1:2369/blog/emails/test?example=1'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.expectStatus(200)
|
||||
.matchBodySnapshot({
|
||||
bulk: {
|
||||
action: 'updateLink',
|
||||
meta: {
|
||||
stats: {
|
||||
successful: 2,
|
||||
unsuccessful: 0
|
||||
},
|
||||
errors: [],
|
||||
unsuccessfulData: []
|
||||
}
|
||||
}
|
||||
})
|
||||
.matchHeaderSnapshot({
|
||||
etag: anyEtag
|
||||
});
|
||||
await agent
|
||||
.get('links')
|
||||
.expectStatus(200)
|
||||
.matchHeaderSnapshot({
|
||||
etag: anyEtag
|
||||
})
|
||||
.matchBodySnapshot({
|
||||
links: [
|
||||
matchLink,
|
||||
{
|
||||
...matchLink,
|
||||
link: {
|
||||
...matchLink.link,
|
||||
to: 'http://127.0.0.1:2369/blog/emails/test?example=1&ref=Test-newsletter&attribution_type=post&attribution_id=618ba1ffbe2896088840a6df'
|
||||
}
|
||||
},
|
||||
{
|
||||
...matchLink,
|
||||
link: {
|
||||
...matchLink.link,
|
||||
to: 'http://127.0.0.1:2369/blog/emails/test?example=1&ref=Test-newsletter&attribution_type=post&attribution_id=618ba1ffbe2896088840a6df'
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('Can bulk update links with external redirect', async function () {
|
||||
const req = await agent.get('links');
|
||||
const siteLink = req.body.links.find((link) => {
|
||||
return link.link.to.includes('subscripe');
|
||||
});
|
||||
const postId = siteLink.post_id;
|
||||
const originalTo = siteLink.link.to;
|
||||
const filter = `post_id:${postId}+to:'${originalTo}'`;
|
||||
await agent
|
||||
.put(`links/bulk/?filter=${encodeURIComponent(filter)}`)
|
||||
.body({
|
||||
bulk: {
|
||||
action: 'updateLink',
|
||||
meta: {
|
||||
link: {
|
||||
to: 'https://example.com/subscribe?ref=Test-newsletter'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.expectStatus(200)
|
||||
.matchBodySnapshot({
|
||||
bulk: {
|
||||
action: 'updateLink',
|
||||
meta: {
|
||||
stats: {
|
||||
successful: 1,
|
||||
unsuccessful: 0
|
||||
},
|
||||
errors: [],
|
||||
unsuccessfulData: []
|
||||
}
|
||||
}
|
||||
})
|
||||
.matchHeaderSnapshot({
|
||||
etag: anyEtag
|
||||
});
|
||||
await agent
|
||||
.get('links')
|
||||
.expectStatus(200)
|
||||
.matchHeaderSnapshot({
|
||||
etag: anyEtag
|
||||
})
|
||||
.matchBodySnapshot({
|
||||
links: [
|
||||
{
|
||||
...matchLink,
|
||||
link: {
|
||||
...matchLink.link,
|
||||
to: 'https://example.com/subscribe?ref=Test-newsletter'
|
||||
}
|
||||
},
|
||||
matchLink,
|
||||
matchLink
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('Can call bulk update link with 0 matches', async function () {
|
||||
const req = await agent.get('links');
|
||||
const siteLink = req.body.links.find((link) => {
|
||||
return link.link.to.includes('subscripe');
|
||||
});
|
||||
const postId = siteLink.post_id;
|
||||
const originalTo = 'https://empty.example.com';
|
||||
const filter = `post_id:${postId}+to:'${originalTo}'`;
|
||||
await agent
|
||||
.put(`links/bulk/?filter=${encodeURIComponent(filter)}`)
|
||||
.body({
|
||||
bulk: {
|
||||
action: 'updateLink',
|
||||
meta: {
|
||||
link: {
|
||||
to: 'https://example.com/subscribe?ref=Test-newsletter'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.expectStatus(200)
|
||||
.matchBodySnapshot({
|
||||
bulk: {
|
||||
action: 'updateLink',
|
||||
meta: {
|
||||
stats: {
|
||||
successful: 0,
|
||||
unsuccessful: 0
|
||||
},
|
||||
errors: [],
|
||||
unsuccessfulData: []
|
||||
}
|
||||
}
|
||||
})
|
||||
.matchHeaderSnapshot({
|
||||
etag: anyEtag
|
||||
});
|
||||
await agent
|
||||
.get('links')
|
||||
.expectStatus(200)
|
||||
.matchHeaderSnapshot({
|
||||
etag: anyEtag
|
||||
})
|
||||
.matchBodySnapshot({
|
||||
links: [
|
||||
{
|
||||
...matchLink,
|
||||
link: {
|
||||
...matchLink.link,
|
||||
to: 'https://example.com/subscripe?ref=Test-newsletter'
|
||||
}
|
||||
},
|
||||
matchLink,
|
||||
matchLink
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,56 @@
|
||||
const should = require('should');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const PostLinkRepository = require('../../../../../core/server/services/link-tracking/PostLinkRepository');
|
||||
|
||||
describe('UNIT: PostLinkRepository class', function () {
|
||||
let postLinkRepository;
|
||||
|
||||
before(function () {
|
||||
postLinkRepository = new PostLinkRepository({
|
||||
LinkRedirect: {
|
||||
getFilteredCollectionQuery: sinon.stub().returns({
|
||||
select: sinon.stub().returns({
|
||||
distinct: sinon.stub().returns([])
|
||||
})
|
||||
}),
|
||||
bulkEdit: sinon.stub().returns({
|
||||
successful: 0,
|
||||
unsuccessful: 0,
|
||||
errors: [],
|
||||
unsuccessfulData: []
|
||||
})
|
||||
},
|
||||
linkRedirectRepository: {}
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe('updateLinks', function () {
|
||||
it('should return correct response format', async function () {
|
||||
const links = await postLinkRepository.updateLinks(['abc'], {
|
||||
to: 'https://example.com',
|
||||
updated_at: new Date('2022-10-20T00:00:00.000Z')
|
||||
}, {});
|
||||
should(links).eql({
|
||||
bulk: {
|
||||
action: 'updateLink',
|
||||
meta: {
|
||||
stats: {
|
||||
successful: 0,
|
||||
unsuccessful: 0
|
||||
},
|
||||
errors: [],
|
||||
unsuccessfulData: []
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -455,6 +455,12 @@ const fixtures = {
|
||||
});
|
||||
},
|
||||
|
||||
insertLinks: function insertLinks() {
|
||||
return Promise.map(DataGenerator.forKnex.links, function (link) {
|
||||
return models.Redirect.add(link, context.internal);
|
||||
});
|
||||
},
|
||||
|
||||
insertEmails: function insertEmails() {
|
||||
return Promise.map(DataGenerator.forKnex.emails, function (email) {
|
||||
return models.Email.add(email, context.internal);
|
||||
@ -803,6 +809,9 @@ const toDoList = {
|
||||
},
|
||||
feedback: function insertFeedback() {
|
||||
return fixtures.insertFeedback();
|
||||
},
|
||||
links: function insertLinks() {
|
||||
return fixtures.insertLinks();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -838,6 +838,30 @@ DataGenerator.Content = {
|
||||
parent_id: '6195c6a1e792de832cd08144',
|
||||
member_index: 1
|
||||
}
|
||||
],
|
||||
|
||||
links: [
|
||||
{
|
||||
id: ObjectId().toHexString(),
|
||||
from: '/r/70b0129a',
|
||||
to: '__GHOST_URL__/blog/email/01cd4df3-83fa-4921-83be-3bb9a465ef83/?ref=Test-newsletter&attribution_id=6343994e7216ffcbce491716&attribution_type=post',
|
||||
created_at: null,
|
||||
updated_at: null
|
||||
},
|
||||
{
|
||||
id: ObjectId().toHexString(),
|
||||
from: '/r/a0b0129a',
|
||||
to: '__GHOST_URL__/blog/email/01cd4df3-83fa-4921-83be-3bb9a465ef83/?ref=Test-newsletter&attribution_id=6343994e7216ffcbce491716&attribution_type=post',
|
||||
created_at: null,
|
||||
updated_at: null
|
||||
},
|
||||
{
|
||||
id: ObjectId().toHexString(),
|
||||
from: '/r/20b0129a',
|
||||
to: 'https://example.com/subscripe?ref=Test-newsletter',
|
||||
created_at: null,
|
||||
updated_at: null
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@ -874,6 +898,9 @@ DataGenerator.Content.members_stripe_customers[4].member_id = DataGenerator.Cont
|
||||
DataGenerator.Content.members_paid_subscription_events[0].member_id = DataGenerator.Content.members[2].id;
|
||||
DataGenerator.Content.members_paid_subscription_events[1].member_id = DataGenerator.Content.members[3].id;
|
||||
DataGenerator.Content.members_paid_subscription_events[2].member_id = DataGenerator.Content.members[4].id;
|
||||
DataGenerator.Content.links[0].post_id = DataGenerator.Content.posts[0].id;
|
||||
DataGenerator.Content.links[1].post_id = DataGenerator.Content.posts[0].id;
|
||||
DataGenerator.Content.links[2].post_id = DataGenerator.Content.posts[0].id;
|
||||
|
||||
DataGenerator.forKnex = (function () {
|
||||
function createBasic(overrides) {
|
||||
@ -1268,6 +1295,14 @@ DataGenerator.forKnex = (function () {
|
||||
});
|
||||
}
|
||||
|
||||
function createLink(overrides) {
|
||||
const newObj = _.cloneDeep(overrides);
|
||||
return _.defaults(newObj, {
|
||||
created_at: new Date(),
|
||||
updated_at: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
const posts = [
|
||||
createPost(DataGenerator.Content.posts[0]),
|
||||
createPost(DataGenerator.Content.posts[1]),
|
||||
@ -1736,6 +1771,12 @@ DataGenerator.forKnex = (function () {
|
||||
createComment(DataGenerator.Content.comments[1])
|
||||
];
|
||||
|
||||
const links = [
|
||||
createLink(DataGenerator.Content.links[0]),
|
||||
createLink(DataGenerator.Content.links[1]),
|
||||
createLink(DataGenerator.Content.links[2])
|
||||
];
|
||||
|
||||
return {
|
||||
createPost,
|
||||
createGenericPost,
|
||||
@ -1797,6 +1838,7 @@ DataGenerator.forKnex = (function () {
|
||||
custom_theme_settings,
|
||||
comments,
|
||||
redirects,
|
||||
links,
|
||||
|
||||
members_paid_subscription_events,
|
||||
members_created_events,
|
||||
|
@ -7,6 +7,7 @@ const LinkRedirect = require('./LinkRedirect');
|
||||
* @typedef {object} ILinkRedirectRepository
|
||||
* @prop {(url: URL) => Promise<LinkRedirect|undefined>} getByURL
|
||||
* @prop {({filter: string}) => Promise<LinkRedirect[]>} getAll
|
||||
* @prop {({filter: string}) => Promise<String[]>} getFilteredIds
|
||||
* @prop {(linkRedirect: LinkRedirect) => Promise<void>} save
|
||||
*/
|
||||
|
||||
@ -47,6 +48,15 @@ class LinkRedirectsService {
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} options
|
||||
*
|
||||
* @returns {Promise<String[]>}
|
||||
*/
|
||||
async getFilteredIds(options) {
|
||||
return await this.#linkRedirectRepository.getFilteredIds(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {URL} from
|
||||
* @param {URL} to
|
||||
|
@ -2,6 +2,10 @@ const {RedirectEvent} = require('@tryghost/link-redirects');
|
||||
const LinkClick = require('./LinkClick');
|
||||
const PostLink = require('./PostLink');
|
||||
const ObjectID = require('bson-objectid').default;
|
||||
const errors = require('@tryghost/errors');
|
||||
const nql = require('@tryghost/nql');
|
||||
const _ = require('lodash');
|
||||
const tpl = require('@tryghost/tpl');
|
||||
|
||||
/**
|
||||
* @typedef {object} ILinkClickRepository
|
||||
@ -25,14 +29,22 @@ const ObjectID = require('bson-objectid').default;
|
||||
* @prop {(to: URL, slug: string) => Promise<ILinkRedirect>} addRedirect
|
||||
* @prop {() => Promise<string>} getSlug
|
||||
* @prop {({filter: string}) => Promise<ILinkRedirect[]>} getAll
|
||||
* @prop {({filter: string}) => Promise<string[]>} getFilteredIds
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} IPostLinkRepository
|
||||
* @prop {(postLink: PostLink) => Promise<void>} save
|
||||
* @prop {({filter: string}) => Promise<FullPostLink[]>} getAll
|
||||
* @prop {(linkIds: array, data, options) => Promise<FullPostLink[]>} updateLinks
|
||||
*/
|
||||
|
||||
const messages = {
|
||||
invalidFilter: 'Invalid filter value received',
|
||||
unsupportedBulkAction: 'Unsupported bulk action',
|
||||
invalidRedirectUrl: 'Invalid redirect URL value'
|
||||
};
|
||||
|
||||
class LinkClickTrackingService {
|
||||
#initialised = false;
|
||||
|
||||
@ -44,6 +56,10 @@ class LinkClickTrackingService {
|
||||
#postLinkRepository;
|
||||
/** @type DomainEvents */
|
||||
#DomainEvents;
|
||||
/** @type {Object} */
|
||||
#LinkRedirect;
|
||||
/** @type {Object} */
|
||||
#urlUtils;
|
||||
|
||||
/**
|
||||
* @param {object} deps
|
||||
@ -51,12 +67,14 @@ class LinkClickTrackingService {
|
||||
* @param {ILinkRedirectService} deps.linkRedirectService
|
||||
* @param {IPostLinkRepository} deps.postLinkRepository
|
||||
* @param {DomainEvents} deps.DomainEvents
|
||||
* @param {urlUtils} deps.urlUtils
|
||||
*/
|
||||
constructor(deps) {
|
||||
this.#linkClickRepository = deps.linkClickRepository;
|
||||
this.#linkRedirectService = deps.linkRedirectService;
|
||||
this.#postLinkRepository = deps.postLinkRepository;
|
||||
this.#DomainEvents = deps.DomainEvents;
|
||||
this.#urlUtils = deps.urlUtils;
|
||||
}
|
||||
|
||||
async init() {
|
||||
@ -78,7 +96,97 @@ class LinkClickTrackingService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* validate and manage the new redirect url in filter
|
||||
* `to` url needs decoding and transformation to relative url for comparision
|
||||
* @param {string} filter
|
||||
* @returns {Object} parsed filter
|
||||
* @throws {errors.BadRequestError}
|
||||
*/
|
||||
#parseLinkFilter(filter) {
|
||||
// decode filter to manage any encoded uri components
|
||||
filter = decodeURIComponent(filter);
|
||||
|
||||
try {
|
||||
const filterJson = nql(filter).parse();
|
||||
const postId = filterJson?.$and?.[0]?.post_id;
|
||||
const redirectUrl = new URL(filterJson?.$and?.[1]?.to);
|
||||
if (!postId || !redirectUrl) {
|
||||
throw new errors.BadRequestError({
|
||||
message: tpl(messages.invalidFilter)
|
||||
});
|
||||
}
|
||||
return {
|
||||
postId,
|
||||
redirectUrl
|
||||
};
|
||||
} catch (e) {
|
||||
throw new errors.BadRequestError({
|
||||
message: tpl(messages.invalidFilter),
|
||||
context: e.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#getRedirectLinkWithAttribution({newLink, oldLink, postId}) {
|
||||
const newUrl = new URL(newLink);
|
||||
const oldUrl = new URL(oldLink);
|
||||
// append newsletter ref query param from oldUrl to newUrl
|
||||
if (oldUrl.searchParams.has('ref')) {
|
||||
newUrl.searchParams.set('ref', oldUrl.searchParams.get('ref'));
|
||||
}
|
||||
|
||||
// append post attribution to site urls
|
||||
const isSite = this.#urlUtils.isSiteUrl(newUrl);
|
||||
if (isSite) {
|
||||
newUrl.searchParams.set('attribution_type', 'post');
|
||||
newUrl.searchParams.set('attribution_id', postId);
|
||||
}
|
||||
return newUrl;
|
||||
}
|
||||
|
||||
async #updateLinks(data, options) {
|
||||
const filterOptions = _.pick(options, ['transacting', 'context', 'filter']);
|
||||
|
||||
// decode and parse filter to manage new redirect url
|
||||
const {postId, redirectUrl} = this.#parseLinkFilter(filterOptions.filter);
|
||||
|
||||
// manages transformation of current url to relative for comparision
|
||||
const transformedOldUrl = this.#urlUtils.absoluteToTransformReady(redirectUrl.href);
|
||||
const filterQuery = `post_id:${postId}+to:'${transformedOldUrl}'`;
|
||||
|
||||
const updatedFilterOptions = {
|
||||
...filterOptions,
|
||||
filter: filterQuery
|
||||
};
|
||||
|
||||
// get new redirect link with proper attribution
|
||||
const newRedirectUrl = this.#getRedirectLinkWithAttribution({
|
||||
newLink: data.meta?.link?.to,
|
||||
oldLink: redirectUrl.href,
|
||||
postId
|
||||
});
|
||||
const linkIds = await this.#linkRedirectService.getFilteredIds(updatedFilterOptions);
|
||||
|
||||
const bulkUpdateOptions = _.pick(options, ['transacting']);
|
||||
const updateData = {
|
||||
to: this.#urlUtils.absoluteToTransformReady(newRedirectUrl.href),
|
||||
updated_at: new Date()
|
||||
};
|
||||
|
||||
return await this.#postLinkRepository.updateLinks(linkIds, updateData, bulkUpdateOptions);
|
||||
}
|
||||
|
||||
async bulkEdit(data, options) {
|
||||
if (data.action === 'updateLink') {
|
||||
return await this.#updateLinks(data, options);
|
||||
}
|
||||
throw new errors.IncorrectUsageError({
|
||||
message: tpl(messages.unsupportedBulkAction)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private (not using # to allow tests)
|
||||
* Replace URL with a redirect that redirects to the original URL, and link that redirect with the given post
|
||||
*/
|
||||
|
@ -1,5 +1,6 @@
|
||||
const LinkClickTrackingService = require('../lib/LinkClickTrackingService');
|
||||
const sinon = require('sinon');
|
||||
const should = require('should');
|
||||
const assert = require('assert');
|
||||
const ObjectID = require('bson-objectid').default;
|
||||
const PostLink = require('../lib/PostLink');
|
||||
@ -34,10 +35,10 @@ describe('LinkClickTrackingService', function () {
|
||||
}
|
||||
});
|
||||
const links = await service.getLinks({filter: 'post_id:1'});
|
||||
|
||||
|
||||
// Check called with filter
|
||||
assert.ok(getAll.calledOnceWithExactly({filter: 'post_id:1'}));
|
||||
|
||||
|
||||
// Check returned value
|
||||
assert.deepStrictEqual(links, ['test']);
|
||||
});
|
||||
@ -66,7 +67,7 @@ describe('LinkClickTrackingService', function () {
|
||||
|
||||
// Check getSlugUrl called
|
||||
assert(getSlugUrl.calledOnce);
|
||||
|
||||
|
||||
// Check save called
|
||||
assert(
|
||||
save.calledOnceWithExactly(
|
||||
@ -102,7 +103,7 @@ describe('LinkClickTrackingService', function () {
|
||||
|
||||
// Check getSlugUrl called
|
||||
assert(getSlugUrl.calledOnce);
|
||||
|
||||
|
||||
// Check save called
|
||||
assert(
|
||||
save.calledOnceWithExactly(
|
||||
@ -168,4 +169,44 @@ describe('LinkClickTrackingService', function () {
|
||||
assert.equal(save.firstCall.args[0].link_id, linkId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkEdit', function () {
|
||||
it('returns the result of updating links', async function () {
|
||||
const service = new LinkClickTrackingService({
|
||||
urlUtils: {
|
||||
absoluteToTransformReady: (d) => {
|
||||
return d;
|
||||
},
|
||||
isSiteUrl: sinon.stub().returns(true)
|
||||
},
|
||||
postLinkRepository: {
|
||||
updateLinks: sinon.stub().resolves({
|
||||
successful: 0,
|
||||
unsuccessful: 0,
|
||||
errors: [],
|
||||
unsuccessfulData: []
|
||||
})
|
||||
},
|
||||
linkRedirectService: {
|
||||
getFilteredIds: sinon.stub().resolves([])
|
||||
}
|
||||
});
|
||||
const options = {
|
||||
filter: `post_id:1+to:'https://test.com'`
|
||||
};
|
||||
|
||||
const result = await service.bulkEdit({
|
||||
action: 'updateLink',
|
||||
meta: {
|
||||
link: {to: 'https://example.com'}
|
||||
}
|
||||
}, options);
|
||||
should(result).eql({
|
||||
successful: 0,
|
||||
unsuccessful: 0,
|
||||
errors: [],
|
||||
unsuccessfulData: []
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user