Added support for deleting mentions (#16238)
refs https://github.com/TryGhost/Team/issues/2534 As we're using soft deletes for mentions we need to store the `deleted` column as well as enforce a `'deleted:false'` filter on the bookshelf model. We've also implemented the handling for deleting mentions. Where we remove a mention anytime we receive and update from or to a page which no longer exists. Co-authored-by: Steve Larson <9larsons@gmail.com>
This commit is contained in:
parent
d20696805f
commit
4e8af72305
@ -4,6 +4,9 @@ const Mention = ghostBookshelf.Model.extend({
|
||||
tableName: 'mentions',
|
||||
defaults: {
|
||||
deleted: false
|
||||
},
|
||||
enforcedFilters() {
|
||||
return 'deleted:false';
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -106,7 +106,8 @@ module.exports = class BookshelfMentionRepository {
|
||||
target: mention.target.href,
|
||||
resource_id: mention.resourceId?.toHexString(),
|
||||
resource_type: mention.resourceId ? 'post' : null,
|
||||
payload: mention.payload ? JSON.stringify(mention.payload) : null
|
||||
payload: mention.payload ? JSON.stringify(mention.payload) : null,
|
||||
deleted: Mention.isDeleted(mention)
|
||||
};
|
||||
|
||||
const existing = await this.#MentionModel.findOne({id: data.id}, {require: false});
|
||||
|
@ -52,7 +52,8 @@ class WebMentionsImporter extends TableImporter {
|
||||
created_at: dateToDatabaseString(timestamp),
|
||||
payload: JSON.stringify({
|
||||
// TODO: Add some random payload
|
||||
})
|
||||
}),
|
||||
deleted: Math.floor(Math.random() * 2) ? true : false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
const nql = require('@tryghost/nql');
|
||||
const Mention = require('./Mention');
|
||||
|
||||
/**
|
||||
* @typedef {import('./Mention')} Mention
|
||||
@ -55,7 +56,7 @@ module.exports = class InMemoryMentionRepository {
|
||||
*/
|
||||
async getBySourceAndTarget(source, target) {
|
||||
return this.#store.find((item) => {
|
||||
return item.source.href === source.href && item.target.href === target.href;
|
||||
return item.source.href === source.href && item.target.href === target.href && !Mention.isDeleted(item);
|
||||
});
|
||||
}
|
||||
|
||||
@ -72,7 +73,7 @@ module.exports = class InMemoryMentionRepository {
|
||||
const data = this.#store.slice();
|
||||
|
||||
const results = data.slice().filter((item) => {
|
||||
return filter.queryJSON(this.toPrimitive(item));
|
||||
return filter.queryJSON(this.toPrimitive(item)) && !Mention.isDeleted(item);
|
||||
});
|
||||
|
||||
if (options.order === 'created_at desc') {
|
||||
|
@ -78,6 +78,11 @@ module.exports = class Mention {
|
||||
return this.#sourceFeaturedImage;
|
||||
}
|
||||
|
||||
#deleted = false;
|
||||
delete() {
|
||||
this.#deleted = true;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
@ -224,6 +229,14 @@ module.exports = class Mention {
|
||||
}
|
||||
return mention;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Mention} mention
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static isDeleted(mention) {
|
||||
return mention.#deleted;
|
||||
}
|
||||
};
|
||||
|
||||
function validateString(value, maxlength, name) {
|
||||
|
@ -133,23 +133,35 @@ module.exports = class MentionsAPI {
|
||||
* @returns {Promise<Mention>}
|
||||
*/
|
||||
async processWebmention(webmention) {
|
||||
const targetExists = await this.#routingService.pageExists(webmention.target);
|
||||
|
||||
if (!targetExists) {
|
||||
throw new errors.BadRequestError({
|
||||
message: `${webmention.target} is not a valid URL for this site.`
|
||||
});
|
||||
}
|
||||
|
||||
const resourceInfo = await this.#resourceService.getByURL(webmention.target);
|
||||
|
||||
const metadata = await this.#webmentionMetadata.fetch(webmention.source);
|
||||
|
||||
let mention = await this.#repository.getBySourceAndTarget(
|
||||
webmention.source,
|
||||
webmention.target
|
||||
);
|
||||
|
||||
const targetExists = await this.#routingService.pageExists(webmention.target);
|
||||
|
||||
if (!targetExists) {
|
||||
if (!mention) {
|
||||
throw new errors.BadRequestError({
|
||||
message: `${webmention.target} is not a valid URL for this site.`
|
||||
});
|
||||
} else {
|
||||
mention.delete();
|
||||
}
|
||||
}
|
||||
|
||||
const resourceInfo = await this.#resourceService.getByURL(webmention.target);
|
||||
|
||||
let metadata;
|
||||
try {
|
||||
metadata = await this.#webmentionMetadata.fetch(webmention.source);
|
||||
} catch (err) {
|
||||
if (!mention) {
|
||||
throw err;
|
||||
}
|
||||
mention.delete();
|
||||
}
|
||||
|
||||
if (!mention) {
|
||||
mention = await Mention.create({
|
||||
source: webmention.source,
|
||||
|
@ -76,4 +76,35 @@ describe('InMemoryMentionRepository', function () {
|
||||
assert(pageThree.meta.pagination.prev === 2);
|
||||
assert(pageThree.meta.pagination.next === null);
|
||||
});
|
||||
|
||||
describe(`GetPage`, function () {
|
||||
it(`Doesn't return deleted mentions`, async function () {
|
||||
const repository = new InMemoryMentionRepository();
|
||||
|
||||
const validInput = {
|
||||
source: 'https://source.com',
|
||||
target: 'https://target.com',
|
||||
sourceTitle: 'Title!',
|
||||
sourceExcerpt: 'Excerpt!'
|
||||
};
|
||||
|
||||
const mentions = await Promise.all([
|
||||
Mention.create(validInput),
|
||||
Mention.create(validInput)
|
||||
]);
|
||||
|
||||
for (const mention of mentions) {
|
||||
await repository.save(mention);
|
||||
}
|
||||
|
||||
const pageOne = await repository.getPage({page: 1, limit: 'all'});
|
||||
assert(pageOne.meta.pagination.total === 2);
|
||||
|
||||
mentions[0].delete();
|
||||
await repository.save(mentions[0]);
|
||||
|
||||
const pageTwo = await repository.getPage({page: 1, limit: 'all'});
|
||||
assert(pageTwo.meta.pagination.total === 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -285,4 +285,136 @@ describe('MentionsAPI', function () {
|
||||
|
||||
assert.equal(page.data[0].id, mention.id);
|
||||
});
|
||||
|
||||
it('Will delete an existing mention if the target page does not exist', async function () {
|
||||
const repository = new InMemoryMentionRepository();
|
||||
const api = new MentionsAPI({
|
||||
repository,
|
||||
routingService: {
|
||||
pageExists: sinon.stub().onFirstCall().resolves(true).onSecondCall().resolves(false)
|
||||
},
|
||||
resourceService: {
|
||||
async getByURL() {
|
||||
return {
|
||||
type: 'post',
|
||||
id: new ObjectID
|
||||
};
|
||||
}
|
||||
},
|
||||
webmentionMetadata: mockWebmentionMetadata
|
||||
});
|
||||
|
||||
checkFirstMention: {
|
||||
const mention = await api.processWebmention({
|
||||
source: new URL('https://source.com'),
|
||||
target: new URL('https://target.com'),
|
||||
payload: {}
|
||||
});
|
||||
|
||||
const page = await api.listMentions({
|
||||
limit: 'all'
|
||||
});
|
||||
|
||||
assert.equal(page.data[0].id, mention.id);
|
||||
break checkFirstMention;
|
||||
}
|
||||
|
||||
checkMentionDeleted: {
|
||||
await api.processWebmention({
|
||||
source: new URL('https://source.com'),
|
||||
target: new URL('https://target.com'),
|
||||
payload: {}
|
||||
});
|
||||
|
||||
const page = await api.listMentions({
|
||||
limit: 'all'
|
||||
});
|
||||
|
||||
assert.equal(page.data.length, 0);
|
||||
break checkMentionDeleted;
|
||||
}
|
||||
});
|
||||
|
||||
it('Will delete an existing mention if the source page does not exist', async function () {
|
||||
const repository = new InMemoryMentionRepository();
|
||||
const api = new MentionsAPI({
|
||||
repository,
|
||||
routingService: mockRoutingService,
|
||||
resourceService: {
|
||||
async getByURL() {
|
||||
return {
|
||||
type: 'post',
|
||||
id: new ObjectID
|
||||
};
|
||||
}
|
||||
},
|
||||
webmentionMetadata: {
|
||||
fetch: sinon.stub()
|
||||
.onFirstCall().resolves(mockWebmentionMetadata.fetch())
|
||||
.onSecondCall().rejects()
|
||||
}
|
||||
});
|
||||
|
||||
checkFirstMention: {
|
||||
const mention = await api.processWebmention({
|
||||
source: new URL('https://source.com'),
|
||||
target: new URL('https://target.com'),
|
||||
payload: {}
|
||||
});
|
||||
|
||||
const page = await api.listMentions({
|
||||
limit: 'all'
|
||||
});
|
||||
|
||||
assert.equal(page.data[0].id, mention.id);
|
||||
break checkFirstMention;
|
||||
}
|
||||
|
||||
checkMentionDeleted: {
|
||||
await api.processWebmention({
|
||||
source: new URL('https://source.com'),
|
||||
target: new URL('https://target.com'),
|
||||
payload: {}
|
||||
});
|
||||
|
||||
const page = await api.listMentions({
|
||||
limit: 'all'
|
||||
});
|
||||
|
||||
assert.equal(page.data.length, 0);
|
||||
break checkMentionDeleted;
|
||||
}
|
||||
});
|
||||
|
||||
it('Will throw for new mentions if the source page is not found', async function () {
|
||||
const repository = new InMemoryMentionRepository();
|
||||
const api = new MentionsAPI({
|
||||
repository,
|
||||
routingService: mockRoutingService,
|
||||
resourceService: {
|
||||
async getByURL() {
|
||||
return {
|
||||
type: 'post',
|
||||
id: new ObjectID
|
||||
};
|
||||
}
|
||||
},
|
||||
webmentionMetadata: {
|
||||
fetch: sinon.stub().rejects(new Error(''))
|
||||
}
|
||||
});
|
||||
|
||||
let error = null;
|
||||
try {
|
||||
await api.processWebmention({
|
||||
source: new URL('https://source.com'),
|
||||
target: new URL('https://target.com'),
|
||||
payload: {}
|
||||
});
|
||||
} catch (err) {
|
||||
error = err;
|
||||
} finally {
|
||||
assert(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user