ghost 4.43.0 → 4.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/boot.js +2 -0
- package/core/built/assets/ghost-dark-887882218a8f9a4a367de52212d27917.css +1 -0
- package/core/built/assets/ghost.min-0b3ecc9dd9e8b3b380d93f1839213af5.css +1 -0
- package/core/built/assets/{ghost.min-2a278873d60d6a13a4c05a396e5bed5e.js → ghost.min-aafce1ab3f2ab6b4a385e8b888548e15.js} +391 -290
- package/core/built/assets/img/abstract-2-2937e2902b64360d0cbe4cec8bd8479b.jpg +0 -0
- package/core/built/assets/img/abstract-c52b2f4208e7fd2e7b8abd8b1eec4f7b.jpg +0 -0
- package/core/built/assets/img/community-be8c1dcecfb157f2bfba5cababc8e686.jpg +0 -0
- package/core/built/assets/{vendor.min-21f79c68a284acb1b70039f3f63e5507.js → vendor.min-eaf9e7b39e2ba76722eabc7a814e0ff1.js} +103 -94
- package/core/frontend/apps/amp/lib/helpers/amp_content.js +2 -3
- package/core/frontend/web/middleware/cors.js +56 -0
- package/core/frontend/web/middleware/index.js +1 -0
- package/core/frontend/web/middleware/static-theme.js +8 -8
- package/core/frontend/web/site.js +1 -48
- package/core/server/api/canary/authentication.js +2 -2
- package/core/server/api/canary/newsletters.js +32 -0
- package/core/server/api/canary/posts.js +1 -0
- package/core/server/api/canary/stats.js +2 -2
- package/core/server/api/canary/utils/serializers/output/members.js +3 -0
- package/core/server/api/shared/http.js +1 -1
- package/core/server/data/importer/importers/data/settings.js +0 -3
- package/core/server/data/migrations/versions/4.43/2022-03-28-19-26-recreate-newsletter-table.js +4 -4
- package/core/server/data/migrations/versions/4.44/2022-04-06-15-22-populate-type-column-for-paid-subscription-events.js +21 -0
- package/core/server/data/migrations/versions/4.44/2022-04-08-11-54-add-cancelled-events.js +51 -0
- package/core/server/data/migrations/versions/4.44/2022-04-11-08-24-add-newsletter-permissions.js +33 -0
- package/core/server/data/migrations/versions/4.44/2022-04-11-10-54-add-mrr-to-subscriptions.js +8 -0
- package/core/server/data/migrations/versions/4.44/2022-04-12-07-33-fill-mrr.js +29 -0
- package/core/server/data/migrations/versions/4.44/2022-04-13-12-00-remove-newsletter-sender-name-not-null-constraint.js +33 -0
- package/core/server/data/migrations/versions/4.44/2022-04-15-07-53-add-offer-id-to-subscriptions.js +9 -0
- package/core/server/data/migrations/versions/4.45/2022-04-19-12-23-backfill-subscriptions-offers.js +60 -0
- package/core/server/data/migrations/versions/4.45/2022-04-20-11-25-add-newsletter-read-permission.js +9 -0
- package/core/server/data/migrations/versions/4.45/2022-04-21-02-55-add-notifications-key-entry-to-settings-table.js +8 -0
- package/core/server/data/schema/default-settings/default-settings.json +4 -0
- package/core/server/data/schema/fixtures/fixtures.json +31 -1
- package/core/server/data/schema/schema.js +7 -5
- package/core/server/models/label.js +1 -1
- package/core/server/models/member.js +3 -0
- package/core/server/models/newsletter.js +9 -2
- package/core/server/models/post.js +9 -2
- package/core/server/models/role.js +1 -1
- package/core/server/models/stripe-customer-subscription.js +4 -0
- package/core/server/models/tag.js +1 -1
- package/core/server/models/user.js +1 -1
- package/core/server/services/api-version-compatibility/index.js +26 -0
- package/core/server/services/auth/setup.js +17 -7
- package/core/server/services/mega/mega.js +3 -1
- package/core/server/services/members/middleware.js +10 -2
- package/core/server/services/members/service.js +3 -11
- package/core/server/services/posts/posts-service.js +20 -1
- package/core/server/services/stats/service.js +2 -6
- package/core/server/web/admin/views/default-prod.html +4 -4
- package/core/server/web/admin/views/default.html +4 -4
- package/core/server/web/api/app.js +3 -0
- package/core/server/web/api/canary/admin/app.js +3 -0
- package/core/server/web/api/canary/admin/routes.js +1 -0
- package/core/server/web/api/canary/content/app.js +3 -0
- package/core/server/web/api/middleware/cors.js +1 -1
- package/core/server/web/api/v2/admin/app.js +3 -0
- package/core/server/web/api/v2/content/app.js +3 -0
- package/core/server/web/api/v3/admin/app.js +3 -0
- package/core/server/web/api/v3/content/app.js +3 -0
- package/core/shared/config/defaults.json +2 -2
- package/core/shared/labs.js +3 -1
- package/core/shared/settings-cache/public.js +1 -1
- package/package.json +39 -35
- package/yarn.lock +367 -311
- package/core/built/assets/ghost-dark-1933079797e24ccb8839657020830be5.css +0 -1
- package/core/built/assets/ghost.min-38f3c38c0c6a1864f57079b068a0b0ce.css +0 -1
- package/core/server/services/stats/lib/members-stats-service.js +0 -161
- package/core/server/services/stats/lib/mrr-stats-service.js +0 -154
|
@@ -187,9 +187,8 @@ function ampContent() {
|
|
|
187
187
|
// Use cheerio to traverse through HTML and make little clean-ups
|
|
188
188
|
$ = cheerio.load(ampHTML);
|
|
189
189
|
|
|
190
|
-
// We have to remove source children in video, as source
|
|
191
|
-
//
|
|
192
|
-
// errors in video, because video will be stripped out.
|
|
190
|
+
// We have to remove source children in video, as source is allowed for audio,
|
|
191
|
+
// but causes validation errors in video, because video will be stripped out.
|
|
193
192
|
// @TODO: remove this, when Amperize support video transform
|
|
194
193
|
$('video').children('source').remove();
|
|
195
194
|
$('video').children('track').remove();
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const {URL} = require('url');
|
|
2
|
+
const cors = require('cors');
|
|
3
|
+
const errors = require('@tryghost/errors');
|
|
4
|
+
const config = require('../../../shared/config');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Dynamically configures the expressjs/cors middleware
|
|
8
|
+
*
|
|
9
|
+
* @param {import('express').Request} req
|
|
10
|
+
* @param {Function} callback
|
|
11
|
+
*/
|
|
12
|
+
function corsOptionsDelegate(req, callback) {
|
|
13
|
+
const origin = req.header('Origin');
|
|
14
|
+
const corsOptions = {
|
|
15
|
+
origin: false, // disallow cross-origin requests by default
|
|
16
|
+
credentials: true // required to allow admin-client to login to private sites
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
if (!origin || origin === 'null') {
|
|
20
|
+
return callback(null, corsOptions);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let originUrl;
|
|
24
|
+
try {
|
|
25
|
+
originUrl = new URL(origin);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
return callback(new errors.BadRequestError({err}));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// originUrl will definitely exist here because according to WHATWG URL spec
|
|
31
|
+
// The class constructor will either throw a TypeError or return a URL object
|
|
32
|
+
// https://url.spec.whatwg.org/#url-class
|
|
33
|
+
|
|
34
|
+
// allow all localhost and 127.0.0.1 requests no matter the port
|
|
35
|
+
if (originUrl.hostname === 'localhost' || originUrl.hostname === '127.0.0.1') {
|
|
36
|
+
corsOptions.origin = true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// allow the configured host through on any protocol
|
|
40
|
+
const siteUrl = new URL(config.get('url'));
|
|
41
|
+
if (originUrl.host === siteUrl.host) {
|
|
42
|
+
corsOptions.origin = true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// allow the configured admin:url host through on any protocol
|
|
46
|
+
if (config.get('admin:url')) {
|
|
47
|
+
const adminUrl = new URL(config.get('admin:url'));
|
|
48
|
+
if (originUrl.host === adminUrl.host) {
|
|
49
|
+
corsOptions.origin = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
callback(null, corsOptions);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = cors(corsOptionsDelegate);
|
|
@@ -4,18 +4,18 @@ const constants = require('@tryghost/constants');
|
|
|
4
4
|
const themeEngine = require('../../services/theme-engine');
|
|
5
5
|
const express = require('../../../shared/express');
|
|
6
6
|
|
|
7
|
-
function
|
|
8
|
-
const
|
|
7
|
+
function isDeniedFile(file) {
|
|
8
|
+
const deniedFileTypes = ['.hbs', '.md', '.json'];
|
|
9
9
|
const ext = path.extname(file);
|
|
10
10
|
|
|
11
|
-
return
|
|
11
|
+
return deniedFileTypes.includes(ext);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
function
|
|
15
|
-
const
|
|
14
|
+
function isAllowedFile(file) {
|
|
15
|
+
const allowedFiles = ['manifest.json'];
|
|
16
16
|
const base = path.basename(file);
|
|
17
17
|
|
|
18
|
-
return
|
|
18
|
+
return allowedFiles.includes(base);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
function forwardToExpressStatic(req, res, next) {
|
|
@@ -31,8 +31,8 @@ function forwardToExpressStatic(req, res, next) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
function staticTheme() {
|
|
34
|
-
return function
|
|
35
|
-
if (!
|
|
34
|
+
return function denyStatic(req, res, next) {
|
|
35
|
+
if (!isAllowedFile(req.path) && isDeniedFile(req.path)) {
|
|
36
36
|
return next();
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
const debug = require('@tryghost/debug')('frontend');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const express = require('../../shared/express');
|
|
4
|
-
const cors = require('cors');
|
|
5
|
-
const {URL} = require('url');
|
|
6
|
-
const errors = require('@tryghost/errors');
|
|
7
4
|
const DomainEvents = require('@tryghost/domain-events');
|
|
8
5
|
const {MemberPageViewEvent} = require('@tryghost/member-events');
|
|
9
6
|
|
|
@@ -31,50 +28,6 @@ const STATIC_FILES_URL_PREFIX = `/${constants.STATIC_FILES_URL_PREFIX}`;
|
|
|
31
28
|
|
|
32
29
|
let router;
|
|
33
30
|
|
|
34
|
-
const corsOptionsDelegate = function corsOptionsDelegate(req, callback) {
|
|
35
|
-
const origin = req.header('Origin');
|
|
36
|
-
const corsOptions = {
|
|
37
|
-
origin: false, // disallow cross-origin requests by default
|
|
38
|
-
credentials: true // required to allow admin-client to login to private sites
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
if (!origin || origin === 'null') {
|
|
42
|
-
return callback(null, corsOptions);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
let originUrl;
|
|
46
|
-
try {
|
|
47
|
-
originUrl = new URL(origin);
|
|
48
|
-
} catch (err) {
|
|
49
|
-
return callback(new errors.BadRequestError({err}));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// originUrl will definitely exist here because according to WHATWG URL spec
|
|
53
|
-
// The class constructor will either throw a TypeError or return a URL object
|
|
54
|
-
// https://url.spec.whatwg.org/#url-class
|
|
55
|
-
|
|
56
|
-
// allow all localhost and 127.0.0.1 requests no matter the port
|
|
57
|
-
if (originUrl.hostname === 'localhost' || originUrl.hostname === '127.0.0.1') {
|
|
58
|
-
corsOptions.origin = true;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// allow the configured host through on any protocol
|
|
62
|
-
const siteUrl = new URL(config.get('url'));
|
|
63
|
-
if (originUrl.host === siteUrl.host) {
|
|
64
|
-
corsOptions.origin = true;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// allow the configured admin:url host through on any protocol
|
|
68
|
-
if (config.get('admin:url')) {
|
|
69
|
-
const adminUrl = new URL(config.get('admin:url'));
|
|
70
|
-
if (originUrl.host === adminUrl.host) {
|
|
71
|
-
corsOptions.origin = true;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
callback(null, corsOptions);
|
|
76
|
-
};
|
|
77
|
-
|
|
78
31
|
function SiteRouter(req, res, next) {
|
|
79
32
|
router(req, res, next);
|
|
80
33
|
}
|
|
@@ -89,7 +42,7 @@ module.exports = function setupSiteApp(options = {}) {
|
|
|
89
42
|
siteApp.set('view engine', 'hbs');
|
|
90
43
|
|
|
91
44
|
// enable CORS headers (allows admin client to hit front-end when configured on separate URLs)
|
|
92
|
-
siteApp.use(cors
|
|
45
|
+
siteApp.use(mw.cors);
|
|
93
46
|
|
|
94
47
|
siteApp.use(offersService.middleware);
|
|
95
48
|
|
|
@@ -51,14 +51,14 @@ module.exports = {
|
|
|
51
51
|
})
|
|
52
52
|
.then((data) => {
|
|
53
53
|
try {
|
|
54
|
-
return auth.setup.doFixtures(data
|
|
54
|
+
return auth.setup.doFixtures(data);
|
|
55
55
|
} catch (e) {
|
|
56
56
|
return data;
|
|
57
57
|
}
|
|
58
58
|
})
|
|
59
59
|
.then((data) => {
|
|
60
60
|
try {
|
|
61
|
-
return auth.setup.
|
|
61
|
+
return auth.setup.doProductAndNewsletter(data, api);
|
|
62
62
|
} catch (e) {
|
|
63
63
|
return data;
|
|
64
64
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
const models = require('../../models');
|
|
2
|
+
const tpl = require('@tryghost/tpl');
|
|
3
|
+
const errors = require('@tryghost/errors');
|
|
4
|
+
|
|
5
|
+
const messages = {
|
|
6
|
+
newsletterNotFound: 'Newsletter not found.'
|
|
7
|
+
};
|
|
2
8
|
|
|
3
9
|
module.exports = {
|
|
4
10
|
docName: 'newsletters',
|
|
@@ -17,6 +23,32 @@ module.exports = {
|
|
|
17
23
|
}
|
|
18
24
|
},
|
|
19
25
|
|
|
26
|
+
read: {
|
|
27
|
+
options: [
|
|
28
|
+
'fields',
|
|
29
|
+
'debug',
|
|
30
|
+
// NOTE: only for internal context
|
|
31
|
+
'forUpdate',
|
|
32
|
+
'transacting'
|
|
33
|
+
],
|
|
34
|
+
data: [
|
|
35
|
+
'id',
|
|
36
|
+
'slug',
|
|
37
|
+
'uuid'
|
|
38
|
+
],
|
|
39
|
+
permissions: true,
|
|
40
|
+
async query(frame) {
|
|
41
|
+
const newsletter = models.Newsletter.findOne(frame.data, frame.options);
|
|
42
|
+
|
|
43
|
+
if (!newsletter) {
|
|
44
|
+
throw new errors.NotFoundError({
|
|
45
|
+
message: tpl(messages.newsletterNotFound)
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return newsletter;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
|
|
20
52
|
add: {
|
|
21
53
|
statusCode: 201,
|
|
22
54
|
permissions: true,
|
|
@@ -8,7 +8,7 @@ module.exports = {
|
|
|
8
8
|
method: 'browse'
|
|
9
9
|
},
|
|
10
10
|
async query() {
|
|
11
|
-
return await statsService.
|
|
11
|
+
return await statsService.getMemberCountHistory();
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
mrr: {
|
|
@@ -17,7 +17,7 @@ module.exports = {
|
|
|
17
17
|
method: 'browse'
|
|
18
18
|
},
|
|
19
19
|
async query() {
|
|
20
|
-
return await statsService.
|
|
20
|
+
return await statsService.getMRRHistory();
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
};
|
|
@@ -133,6 +133,9 @@ function serializeMember(member, options) {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
if (json.newsletters && labsService.isSet('multipleNewsletters')) {
|
|
136
|
+
json.newsletters.sort((a, b) => {
|
|
137
|
+
return a.sort_order - b.sort_order;
|
|
138
|
+
});
|
|
136
139
|
serialized.newsletters = json.newsletters;
|
|
137
140
|
}
|
|
138
141
|
|
|
@@ -83,7 +83,7 @@ const http = (apiImpl) => {
|
|
|
83
83
|
|
|
84
84
|
// CASE: generate headers based on the api ctrl configuration
|
|
85
85
|
if (req && req.headers && req.headers['accept-version'] && res.locals) {
|
|
86
|
-
headers['
|
|
86
|
+
headers['Content-Version'] = `v${res.locals.safeVersion}`;
|
|
87
87
|
}
|
|
88
88
|
res.set(headers);
|
|
89
89
|
|
package/core/server/data/migrations/versions/4.43/2022-03-28-19-26-recreate-newsletter-table.js
CHANGED
|
@@ -7,7 +7,7 @@ module.exports = recreateTable('newsletters', {
|
|
|
7
7
|
slug: {type: 'string', maxlength: 191, nullable: false, unique: true},
|
|
8
8
|
sender_name: {type: 'string', maxlength: 191, nullable: false},
|
|
9
9
|
sender_email: {type: 'string', maxlength: 191, nullable: true},
|
|
10
|
-
sender_reply_to: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'newsletter', validations: {isIn: ['newsletter', 'support']}},
|
|
10
|
+
sender_reply_to: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'newsletter', validations: {isIn: [['newsletter', 'support']]}},
|
|
11
11
|
status: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'active'},
|
|
12
12
|
visibility: {
|
|
13
13
|
type: 'string',
|
|
@@ -20,10 +20,10 @@ module.exports = recreateTable('newsletters', {
|
|
|
20
20
|
header_image: {type: 'string', maxlength: 2000, nullable: true},
|
|
21
21
|
show_header_icon: {type: 'bool', nullable: false, defaultTo: true},
|
|
22
22
|
show_header_title: {type: 'bool', nullable: false, defaultTo: true},
|
|
23
|
-
title_font_category: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'sans_serif', validations: {isIn: ['serif', 'sans_serif']}},
|
|
24
|
-
title_alignment: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'center', validations: {isIn: ['center', 'left']}},
|
|
23
|
+
title_font_category: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'sans_serif', validations: {isIn: [['serif', 'sans_serif']]}},
|
|
24
|
+
title_alignment: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'center', validations: {isIn: [['center', 'left']]}},
|
|
25
25
|
show_feature_image: {type: 'bool', nullable: false, defaultTo: true},
|
|
26
|
-
body_font_category: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'sans_serif', validations: {isIn: ['serif', 'sans_serif']}},
|
|
26
|
+
body_font_category: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'sans_serif', validations: {isIn: [['serif', 'sans_serif']]}},
|
|
27
27
|
footer_content: {type: 'text', maxlength: 1000000000, nullable: true},
|
|
28
28
|
show_badge: {type: 'bool', nullable: false, defaultTo: true}
|
|
29
29
|
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
|
|
3
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
4
|
+
|
|
5
|
+
module.exports = createTransactionalMigration(
|
|
6
|
+
async function up(knex) {
|
|
7
|
+
logging.info('Setting "type" to "updated" for events with different to_plan & from_plan');
|
|
8
|
+
await knex('members_paid_subscription_events').update('type', 'updated').whereNotNull('from_plan').whereNotNull('to_plan').whereRaw('to_plan != from_plan');
|
|
9
|
+
|
|
10
|
+
logging.info('Setting "type" to "expired" for events with null to_plan or the same to_plan & from_plan');
|
|
11
|
+
await knex('members_paid_subscription_events').update('type', 'expired').whereNull('to_plan').whereNotNull('from_plan');
|
|
12
|
+
await knex('members_paid_subscription_events').update('type', 'expired').whereRaw('from_plan = to_plan');
|
|
13
|
+
|
|
14
|
+
logging.info('Setting "type" to "created" for events with null from_plan');
|
|
15
|
+
await knex('members_paid_subscription_events').update('type', 'created').whereNull('from_plan').whereNotNull('to_plan');
|
|
16
|
+
},
|
|
17
|
+
async function down(knex) {
|
|
18
|
+
logging.info('Setting "type" to null for all rows in "members_paid_subscriptions events"');
|
|
19
|
+
await knex('members_paid_subscription_events').update('type', null);
|
|
20
|
+
}
|
|
21
|
+
);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const ObjectID = require('bson-objectid').default;
|
|
2
|
+
const logging = require('@tryghost/logging');
|
|
3
|
+
|
|
4
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
5
|
+
|
|
6
|
+
module.exports = createTransactionalMigration(
|
|
7
|
+
async function up(knex) {
|
|
8
|
+
const cancelledSubscriptions = await knex
|
|
9
|
+
.select(
|
|
10
|
+
'members.id as member_id',
|
|
11
|
+
'members_stripe_customers_subscriptions.id',
|
|
12
|
+
'members_stripe_customers_subscriptions.stripe_price_id',
|
|
13
|
+
'members_stripe_customers_subscriptions.plan_currency',
|
|
14
|
+
'members_stripe_customers_subscriptions.updated_at'
|
|
15
|
+
)
|
|
16
|
+
.from('members_stripe_customers_subscriptions')
|
|
17
|
+
.join('members_stripe_customers', 'members_stripe_customers.customer_id', '=', 'members_stripe_customers_subscriptions.customer_id')
|
|
18
|
+
.join('members', 'members_stripe_customers.member_id', '=', 'members.id')
|
|
19
|
+
.where('members_stripe_customers_subscriptions.cancel_at_period_end', true)
|
|
20
|
+
.whereNot('members_stripe_customers_subscriptions.status', 'canceled');
|
|
21
|
+
|
|
22
|
+
if (cancelledSubscriptions.length === 0) {
|
|
23
|
+
logging.info('No missing cancelled events - skipping migration');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const eventsToInsert = cancelledSubscriptions.map((subscription) => {
|
|
28
|
+
const event = {
|
|
29
|
+
id: (new ObjectID()).toHexString(),
|
|
30
|
+
type: 'canceled',
|
|
31
|
+
member_id: subscription.member_id,
|
|
32
|
+
subscription_id: subscription.id,
|
|
33
|
+
from_plan: subscription.stripe_price_id,
|
|
34
|
+
to_plan: subscription.stripe_price_id,
|
|
35
|
+
currency: subscription.plan_currency,
|
|
36
|
+
source: 'migration',
|
|
37
|
+
mrr_delta: 0,
|
|
38
|
+
created_at: subscription.updated_at
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return event;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
logging.info(`Found ${eventsToInsert.length} missing cancellation events`);
|
|
45
|
+
await knex('members_paid_subscription_events').insert(eventsToInsert);
|
|
46
|
+
},
|
|
47
|
+
async function down(knex) {
|
|
48
|
+
logging.info('Deleting all members_paid_subscription_events with a "type" of "cancelled"');
|
|
49
|
+
await knex('members_paid_subscription_events').where({type: 'canceled', source: 'migration'}).del();
|
|
50
|
+
}
|
|
51
|
+
);
|
package/core/server/data/migrations/versions/4.44/2022-04-11-08-24-add-newsletter-permissions.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const {
|
|
2
|
+
addPermissionWithRoles,
|
|
3
|
+
combineTransactionalMigrations
|
|
4
|
+
} = require('../../utils');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* This is similar to core/server/data/migrations/versions/4.42/2022-03-30-15-44-add-newsletter-permissions.js
|
|
8
|
+
* as the permissions were not added in the fixture file at the time of the migration.
|
|
9
|
+
* This means the new Ghost installs do not have the newsletter permission and we need this migration.
|
|
10
|
+
*/
|
|
11
|
+
module.exports = combineTransactionalMigrations(
|
|
12
|
+
addPermissionWithRoles({
|
|
13
|
+
name: 'Browse newsletters',
|
|
14
|
+
action: 'browse',
|
|
15
|
+
object: 'newsletter'
|
|
16
|
+
}, [
|
|
17
|
+
'Administrator'
|
|
18
|
+
]),
|
|
19
|
+
addPermissionWithRoles({
|
|
20
|
+
name: 'Add newsletters',
|
|
21
|
+
action: 'add',
|
|
22
|
+
object: 'newsletter'
|
|
23
|
+
}, [
|
|
24
|
+
'Administrator'
|
|
25
|
+
]),
|
|
26
|
+
addPermissionWithRoles({
|
|
27
|
+
name: 'Edit newsletters',
|
|
28
|
+
action: 'edit',
|
|
29
|
+
object: 'newsletter'
|
|
30
|
+
}, [
|
|
31
|
+
'Administrator'
|
|
32
|
+
])
|
|
33
|
+
);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
|
|
3
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
4
|
+
|
|
5
|
+
module.exports = createTransactionalMigration(
|
|
6
|
+
async function up(knex) {
|
|
7
|
+
logging.info('Setting "mrr" for active subscriptions in "members_stripe_customers_subscriptions"');
|
|
8
|
+
|
|
9
|
+
// Note that we also set the MRR for 'canceled' subscriptions (cancel_at_period_end === true)
|
|
10
|
+
// A different migration will make that change in 5.0
|
|
11
|
+
await knex('members_stripe_customers_subscriptions')
|
|
12
|
+
.update('mrr', knex.raw(`
|
|
13
|
+
CASE WHEN plan_interval = 'year' THEN
|
|
14
|
+
FLOOR(plan_amount / 12)
|
|
15
|
+
WHEN plan_interval = 'week' THEN
|
|
16
|
+
plan_amount * 4
|
|
17
|
+
WHEN plan_interval = 'day' THEN
|
|
18
|
+
plan_amount * 30
|
|
19
|
+
ELSE
|
|
20
|
+
plan_amount
|
|
21
|
+
END
|
|
22
|
+
`))
|
|
23
|
+
.whereNotIn('status', ['trialing', 'incomplete', 'incomplete_expired', 'canceled']);
|
|
24
|
+
},
|
|
25
|
+
async function down(knex) {
|
|
26
|
+
logging.info('Setting "mrr" to 0 for all rows in "members_stripe_customers_subscriptions"');
|
|
27
|
+
await knex('members_stripe_customers_subscriptions').update('mrr', 0);
|
|
28
|
+
}
|
|
29
|
+
);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
const {createNonTransactionalMigration} = require('../../utils');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Note: This doesn't use knex.alterTable as it doesn't work for down migration.
|
|
6
|
+
* It tries to insert a `null` into non `nullable` column while altering column
|
|
7
|
+
*/
|
|
8
|
+
module.exports = createNonTransactionalMigration(
|
|
9
|
+
async function up(knex) {
|
|
10
|
+
logging.info('Dropping NOT NULL constraint for: sender_name in table: newsletters');
|
|
11
|
+
|
|
12
|
+
await knex.schema.table('newsletters', function (table) {
|
|
13
|
+
table.dropColumn('sender_name');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
await knex.schema.table('newsletters', function (table) {
|
|
17
|
+
table.string('sender_name', 191).nullable();
|
|
18
|
+
});
|
|
19
|
+
},
|
|
20
|
+
async function down(knex) {
|
|
21
|
+
logging.info('Adding NOT NULL constraint for: sender_name in table: newsletters');
|
|
22
|
+
|
|
23
|
+
await knex.schema.table('newsletters', function (table) {
|
|
24
|
+
table.dropColumn('sender_name');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
await knex.schema.table('newsletters', function (table) {
|
|
28
|
+
// SQLite doesn't allow adding a non nullable column without any default
|
|
29
|
+
table.string('sender_name', 191).notNullable().defaultTo('Ghost');
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
|
package/core/server/data/migrations/versions/4.45/2022-04-19-12-23-backfill-subscriptions-offers.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
const DatabaseInfo = require('@tryghost/database-info');
|
|
3
|
+
|
|
4
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
5
|
+
|
|
6
|
+
module.exports = createTransactionalMigration(
|
|
7
|
+
async function up(knex) {
|
|
8
|
+
logging.info('Backfilling "offer_id" column in "members_stripe_customers_subscriptions" by matching tier and cadence');
|
|
9
|
+
|
|
10
|
+
const subquery = `
|
|
11
|
+
SELECT
|
|
12
|
+
members_stripe_customers_subscriptions.id as subscription_id,
|
|
13
|
+
offer_redemptions.offer_id as offer_id
|
|
14
|
+
FROM
|
|
15
|
+
members_stripe_customers_subscriptions
|
|
16
|
+
JOIN offer_redemptions ON offer_redemptions.subscription_id = members_stripe_customers_subscriptions.id
|
|
17
|
+
JOIN offers ON offers.id = offer_redemptions.offer_id
|
|
18
|
+
JOIN stripe_prices ON members_stripe_customers_subscriptions.stripe_price_id = stripe_prices.stripe_price_id
|
|
19
|
+
JOIN stripe_products ON stripe_prices.stripe_product_id = stripe_products.stripe_product_id
|
|
20
|
+
WHERE
|
|
21
|
+
offers.product_id = stripe_products.product_id
|
|
22
|
+
AND offers.interval = stripe_prices.interval
|
|
23
|
+
AND members_stripe_customers_subscriptions.offer_id is null
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
if (DatabaseInfo.isSQLite(knex)) {
|
|
27
|
+
// Less optimized for SQLite
|
|
28
|
+
const result = await knex.raw(subquery);
|
|
29
|
+
const updatedRows = result.length;
|
|
30
|
+
const subscriptionsToUpdate = result;
|
|
31
|
+
|
|
32
|
+
logging.info(`Setting the offer_id for ${updatedRows} members_stripe_customers_subscriptions`);
|
|
33
|
+
|
|
34
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
35
|
+
for (const u of subscriptionsToUpdate) {
|
|
36
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
37
|
+
await knex('members_stripe_customers_subscriptions')
|
|
38
|
+
.update('offer_id', u.offer_id)
|
|
39
|
+
.where('id', u.subscription_id);
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
// Single update query
|
|
43
|
+
const query = `
|
|
44
|
+
UPDATE
|
|
45
|
+
members_stripe_customers_subscriptions,
|
|
46
|
+
(${subquery}) as c
|
|
47
|
+
SET members_stripe_customers_subscriptions.offer_id = c.offer_id
|
|
48
|
+
WHERE c.subscription_id = members_stripe_customers_subscriptions.id
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
const result = await knex.raw(query);
|
|
52
|
+
const updatedRows = result[0].affectedRows;
|
|
53
|
+
|
|
54
|
+
logging.info(`Updated ${updatedRows} members_stripe_customers_subscriptions with an offer_id`);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
async function down() {
|
|
58
|
+
// We risk losing data if we would reset offer_id here
|
|
59
|
+
}
|
|
60
|
+
);
|