Skip to content

Commit a5dff42

Browse files
committed
Moved posts service to its own package
fixes https://github.com/TryGhost/Team/issues/2778 It is easier to add extra classes using the latest patterns if it has its own package.
1 parent aca8c58 commit a5dff42

File tree

10 files changed

+343
-143
lines changed

10 files changed

+343
-143
lines changed

ghost/core/core/server/services/posts/posts-service.js

Lines changed: 2 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,123 +1,7 @@
1-
const nql = require('@tryghost/nql');
2-
const {BadRequestError} = require('@tryghost/errors');
3-
const tpl = require('@tryghost/tpl');
4-
5-
const messages = {
6-
invalidVisibilityFilter: 'Invalid visibility filter.',
7-
invalidEmailSegment: 'The email segment parameter doesn\'t contain a valid filter'
8-
};
9-
10-
class PostsService {
11-
constructor({urlUtils, models, isSet, stats, emailService}) {
12-
this.urlUtils = urlUtils;
13-
this.models = models;
14-
this.isSet = isSet;
15-
this.stats = stats;
16-
this.emailService = emailService;
17-
}
18-
19-
async editPost(frame) {
20-
// Make sure the newsletter is matching an active newsletter
21-
// Note that this option is simply ignored if the post isn't published or scheduled
22-
if (frame.options.newsletter && frame.options.email_segment) {
23-
if (frame.options.email_segment !== 'all') {
24-
// check filter is valid
25-
try {
26-
await this.models.Member.findPage({filter: frame.options.email_segment, limit: 1});
27-
} catch (err) {
28-
return Promise.reject(new BadRequestError({
29-
message: tpl(messages.invalidEmailSegment),
30-
context: err.message
31-
}));
32-
}
33-
}
34-
}
35-
36-
const model = await this.models.Post.edit(frame.data.posts[0], frame.options);
37-
38-
/**Handle newsletter email */
39-
if (model.get('newsletter_id')) {
40-
const sendEmail = model.wasChanged() && this.shouldSendEmail(model.get('status'), model.previous('status'));
41-
42-
if (sendEmail) {
43-
let postEmail = model.relations.email;
44-
let email;
45-
46-
if (!postEmail) {
47-
email = await this.emailService.createEmail(model);
48-
} else if (postEmail && postEmail.get('status') === 'failed') {
49-
email = await this.emailService.retryEmail(postEmail);
50-
}
51-
if (email) {
52-
model.set('email', email);
53-
}
54-
}
55-
}
56-
57-
return model;
58-
}
59-
60-
async getProductsFromVisibilityFilter(visibilityFilter) {
61-
try {
62-
const allProducts = await this.models.Product.findAll();
63-
const visibilityFilterJson = nql(visibilityFilter).toJSON();
64-
const productsData = (visibilityFilterJson.product ? [visibilityFilterJson] : visibilityFilterJson.$or) || [];
65-
const tiers = productsData
66-
.map((data) => {
67-
return allProducts.find((p) => {
68-
return p.get('slug') === data.product;
69-
});
70-
}).filter(p => !!p).map((d) => {
71-
return d.toJSON();
72-
});
73-
return tiers;
74-
} catch (err) {
75-
return Promise.reject(new BadRequestError({
76-
message: tpl(messages.invalidVisibilityFilter),
77-
context: err.message
78-
}));
79-
}
80-
}
81-
82-
/**
83-
* Calculates if the email should be tried to be sent out
84-
* @private
85-
* @param {String} currentStatus current status from the post model
86-
* @param {String} previousStatus previous status from the post model
87-
* @returns {Boolean}
88-
*/
89-
shouldSendEmail(currentStatus, previousStatus) {
90-
return (['published', 'sent'].includes(currentStatus))
91-
&& (!['published', 'sent'].includes(previousStatus));
92-
}
93-
94-
handleCacheInvalidation(model) {
95-
let cacheInvalidate;
96-
97-
if (
98-
model.get('status') === 'published' && model.wasChanged() ||
99-
model.get('status') === 'draft' && model.previous('status') === 'published'
100-
) {
101-
cacheInvalidate = true;
102-
} else if (
103-
model.get('status') === 'draft' && model.previous('status') !== 'published' ||
104-
model.get('status') === 'scheduled' && model.wasChanged()
105-
) {
106-
cacheInvalidate = {
107-
value: this.urlUtils.urlFor({
108-
relativeUrl: this.urlUtils.urlJoin('/p', model.get('uuid'), '/')
109-
})
110-
};
111-
} else {
112-
cacheInvalidate = false;
113-
}
114-
115-
return cacheInvalidate;
116-
}
117-
}
1+
const {PostsService} = require('@tryghost/posts-service');
1182

1193
/**
120-
* @returns {PostsService} instance of the PostsService
4+
* @returns {InstanceType<PostsService>} instance of the PostsService
1215
*/
1226
const getPostServiceInstance = () => {
1237
const urlUtils = require('../../../shared/url-utils');

ghost/core/test/unit/server/services/posts/posts-service.test.js

Lines changed: 0 additions & 25 deletions
This file was deleted.

ghost/posts-service/.eslintrc.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
plugins: ['ghost'],
3+
extends: [
4+
'plugin:ghost/node'
5+
]
6+
};

ghost/posts-service/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Posts Service
2+

ghost/posts-service/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
PostsService: require('./lib/PostsService')
3+
};
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
const nql = require('@tryghost/nql');
2+
const {BadRequestError} = require('@tryghost/errors');
3+
const tpl = require('@tryghost/tpl');
4+
5+
const messages = {
6+
invalidVisibilityFilter: 'Invalid visibility filter.',
7+
invalidEmailSegment: 'The email segment parameter doesn\'t contain a valid filter'
8+
};
9+
10+
class PostsService {
11+
constructor({urlUtils, models, isSet, stats, emailService}) {
12+
this.urlUtils = urlUtils;
13+
this.models = models;
14+
this.isSet = isSet;
15+
this.stats = stats;
16+
this.emailService = emailService;
17+
}
18+
19+
async editPost(frame) {
20+
// Make sure the newsletter is matching an active newsletter
21+
// Note that this option is simply ignored if the post isn't published or scheduled
22+
if (frame.options.newsletter && frame.options.email_segment) {
23+
if (frame.options.email_segment !== 'all') {
24+
// check filter is valid
25+
try {
26+
await this.models.Member.findPage({filter: frame.options.email_segment, limit: 1});
27+
} catch (err) {
28+
return Promise.reject(new BadRequestError({
29+
message: tpl(messages.invalidEmailSegment),
30+
context: err.message
31+
}));
32+
}
33+
}
34+
}
35+
36+
const model = await this.models.Post.edit(frame.data.posts[0], frame.options);
37+
38+
/**Handle newsletter email */
39+
if (model.get('newsletter_id')) {
40+
const sendEmail = model.wasChanged() && this.shouldSendEmail(model.get('status'), model.previous('status'));
41+
42+
if (sendEmail) {
43+
let postEmail = model.relations.email;
44+
let email;
45+
46+
if (!postEmail) {
47+
email = await this.emailService.createEmail(model);
48+
} else if (postEmail && postEmail.get('status') === 'failed') {
49+
email = await this.emailService.retryEmail(postEmail);
50+
}
51+
if (email) {
52+
model.set('email', email);
53+
}
54+
}
55+
}
56+
57+
return model;
58+
}
59+
60+
async getProductsFromVisibilityFilter(visibilityFilter) {
61+
try {
62+
const allProducts = await this.models.Product.findAll();
63+
const visibilityFilterJson = nql(visibilityFilter).toJSON();
64+
const productsData = (visibilityFilterJson.product ? [visibilityFilterJson] : visibilityFilterJson.$or) || [];
65+
const tiers = productsData
66+
.map((data) => {
67+
return allProducts.find((p) => {
68+
return p.get('slug') === data.product;
69+
});
70+
}).filter(p => !!p).map((d) => {
71+
return d.toJSON();
72+
});
73+
return tiers;
74+
} catch (err) {
75+
return Promise.reject(new BadRequestError({
76+
message: tpl(messages.invalidVisibilityFilter),
77+
context: err.message
78+
}));
79+
}
80+
}
81+
82+
/**
83+
* Calculates if the email should be tried to be sent out
84+
* @private
85+
* @param {String} currentStatus current status from the post model
86+
* @param {String} previousStatus previous status from the post model
87+
* @returns {Boolean}
88+
*/
89+
shouldSendEmail(currentStatus, previousStatus) {
90+
return (['published', 'sent'].includes(currentStatus))
91+
&& (!['published', 'sent'].includes(previousStatus));
92+
}
93+
94+
handleCacheInvalidation(model) {
95+
let cacheInvalidate;
96+
97+
if (
98+
model.get('status') === 'published' && model.wasChanged() ||
99+
model.get('status') === 'draft' && model.previous('status') === 'published'
100+
) {
101+
cacheInvalidate = true;
102+
} else if (
103+
model.get('status') === 'draft' && model.previous('status') !== 'published' ||
104+
model.get('status') === 'scheduled' && model.wasChanged()
105+
) {
106+
cacheInvalidate = {
107+
value: this.urlUtils.urlFor({
108+
relativeUrl: this.urlUtils.urlJoin('/p', model.get('uuid'), '/')
109+
})
110+
};
111+
} else {
112+
cacheInvalidate = false;
113+
}
114+
115+
return cacheInvalidate;
116+
}
117+
}
118+
119+
module.exports = PostsService;

ghost/posts-service/package.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "@tryghost/posts-service",
3+
"version": "0.0.0",
4+
"repository": "https://github.com/TryGhost/Ghost/tree/main/packages/posts-service",
5+
"author": "Ghost Foundation",
6+
"private": true,
7+
"main": "index.js",
8+
"scripts": {
9+
"dev": "echo \"Implement me!\"",
10+
"test:unit": "NODE_ENV=testing c8 --all --reporter text --reporter cobertura mocha './test/**/*.test.js'",
11+
"test": "yarn test:unit",
12+
"lint:code": "eslint *.js lib/ --ext .js --cache",
13+
"lint": "yarn lint:code && yarn lint:test",
14+
"lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache"
15+
},
16+
"files": [
17+
"index.js",
18+
"lib"
19+
],
20+
"devDependencies": {
21+
"c8": "7.13.0",
22+
"mocha": "10.2.0",
23+
"sinon": "15.0.2"
24+
},
25+
"dependencies": {
26+
"@tryghost/errors": "1.2.21",
27+
"@tryghost/nql": "0.11.0",
28+
"@tryghost/tpl": "0.1.22"
29+
}
30+
}

ghost/posts-service/test/.eslintrc.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
plugins: ['ghost'],
3+
extends: [
4+
'plugin:ghost/test'
5+
]
6+
};
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
const {PostsService} = require('../index');
2+
const assert = require('assert');
3+
4+
describe('Posts Service', function () {
5+
it('Can construct class', function () {
6+
new PostsService({});
7+
});
8+
9+
describe('shouldSendEmail', function () {
10+
it('calculates if an email should be sent', async function () {
11+
const postsService = new PostsService({});
12+
13+
assert.deepEqual([
14+
postsService.shouldSendEmail('published', 'draft'),
15+
postsService.shouldSendEmail('published', 'scheduled'),
16+
postsService.shouldSendEmail('sent', 'draft'),
17+
postsService.shouldSendEmail('sent', 'scheduled'),
18+
19+
postsService.shouldSendEmail('published', 'published'),
20+
postsService.shouldSendEmail('published', 'sent'),
21+
postsService.shouldSendEmail('published', 'published'),
22+
postsService.shouldSendEmail('published', 'sent'),
23+
postsService.shouldSendEmail('sent', 'published'),
24+
postsService.shouldSendEmail('sent', 'sent'),
25+
postsService.shouldSendEmail()
26+
], [
27+
true,
28+
true,
29+
true,
30+
true,
31+
32+
false,
33+
false,
34+
false,
35+
false,
36+
false,
37+
false,
38+
false
39+
]);
40+
});
41+
});
42+
});

0 commit comments

Comments
 (0)