ghost 4.42.0 → 4.43.1
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/content/themes/casper/package.json +2 -3
- package/content/themes/casper/partials/post-card.hbs +1 -1
- package/core/built/assets/ghost-dark-1933079797e24ccb8839657020830be5.css +1 -0
- package/core/built/assets/{ghost.min-20096eef632760c3a2906e243adbd24b.js → ghost.min-2a278873d60d6a13a4c05a396e5bed5e.js} +336 -278
- package/core/built/assets/ghost.min-38f3c38c0c6a1864f57079b068a0b0ce.css +1 -0
- package/core/frontend/apps/amp/lib/helpers/amp_analytics.js +1 -1
- package/core/frontend/apps/amp/lib/helpers/amp_components.js +1 -1
- package/core/frontend/apps/amp/lib/helpers/amp_content.js +1 -1
- package/core/frontend/apps/amp/lib/helpers/amp_style.js +1 -1
- package/core/frontend/apps/amp/lib/router.js +6 -5
- package/core/frontend/apps/private-blogging/lib/helpers/input_password.js +1 -1
- package/core/frontend/apps/private-blogging/lib/router.js +2 -2
- package/core/frontend/helpers/asset.js +1 -1
- package/core/frontend/helpers/author.js +1 -1
- package/core/frontend/helpers/authors.js +1 -1
- package/core/frontend/helpers/body_class.js +1 -1
- package/core/frontend/helpers/cancel_link.js +1 -1
- package/core/frontend/helpers/concat.js +1 -1
- package/core/frontend/helpers/content.js +1 -1
- package/core/frontend/helpers/date.js +1 -1
- package/core/frontend/helpers/encode.js +1 -1
- package/core/frontend/helpers/excerpt.js +1 -1
- package/core/frontend/helpers/facebook_url.js +1 -1
- package/core/frontend/helpers/foreach.js +2 -2
- package/core/frontend/helpers/get.js +1 -1
- package/core/frontend/helpers/ghost_foot.js +1 -1
- package/core/frontend/helpers/ghost_head.js +1 -1
- package/core/frontend/helpers/lang.js +1 -1
- package/core/frontend/helpers/link.js +1 -1
- package/core/frontend/helpers/link_class.js +1 -1
- package/core/frontend/helpers/match.js +1 -1
- package/core/frontend/helpers/navigation.js +1 -1
- package/core/frontend/helpers/pagination.js +1 -1
- package/core/frontend/helpers/plural.js +1 -1
- package/core/frontend/helpers/post_class.js +1 -1
- package/core/frontend/helpers/prev_post.js +6 -5
- package/core/frontend/helpers/products.js +1 -1
- package/core/frontend/helpers/reading_time.js +2 -2
- package/core/frontend/helpers/t.js +1 -1
- package/core/frontend/helpers/tags.js +1 -1
- package/core/frontend/helpers/tiers.js +1 -1
- package/core/frontend/helpers/title.js +1 -1
- package/core/frontend/helpers/twitter_url.js +1 -1
- package/core/frontend/helpers/url.js +1 -1
- package/core/frontend/meta/url.js +4 -4
- package/core/{server/data/schema → frontend/services/data}/checks.js +4 -4
- package/core/frontend/services/{routing/helpers → data}/entry-lookup.js +3 -3
- package/core/frontend/services/{routing/helpers → data}/fetch-data.js +3 -3
- package/core/frontend/services/data/index.js +5 -0
- package/core/frontend/services/{rendering.js → handlebars.js} +2 -1
- package/core/frontend/services/helpers/handlebars.js +1 -1
- package/core/frontend/services/proxy.js +2 -4
- package/core/frontend/services/{routing/helpers → rendering}/context.js +0 -0
- package/core/frontend/services/{routing/helpers → rendering}/error.js +0 -0
- package/core/frontend/services/{routing/helpers → rendering}/format-response.js +1 -1
- package/core/frontend/services/{routing/helpers → rendering}/index.js +0 -8
- package/core/frontend/services/{routing/helpers → rendering}/render-entries.js +1 -1
- package/core/frontend/services/{routing/helpers → rendering}/render-entry.js +1 -1
- package/core/frontend/services/{routing/helpers → rendering}/renderer.js +1 -1
- package/core/frontend/services/{routing/helpers → rendering}/secure.js +0 -0
- package/core/frontend/services/{routing/helpers → rendering}/templates.js +2 -2
- package/core/frontend/services/routing/CollectionRouter.js +1 -1
- package/core/frontend/services/routing/controllers/channel.js +9 -9
- package/core/frontend/services/routing/controllers/collection.js +9 -9
- package/core/frontend/services/routing/controllers/email-post.js +5 -6
- package/core/frontend/services/routing/controllers/entry.js +6 -6
- package/core/frontend/services/routing/controllers/preview.js +5 -6
- package/core/frontend/services/routing/controllers/rss.js +4 -3
- package/core/frontend/services/routing/controllers/static.js +5 -5
- package/core/frontend/services/routing/controllers/unsubscribe.js +2 -2
- package/core/frontend/services/routing/index.js +0 -4
- package/core/frontend/web/middleware/error-handler.js +2 -2
- package/core/server/api/canary/stats.js +9 -0
- package/core/server/api/canary/utils/serializers/output/members.js +5 -0
- package/core/server/api/canary/utils/validators/input/index.js +6 -0
- package/core/server/api/shared/http.js +52 -51
- package/core/server/data/exporter/table-lists.js +1 -0
- package/core/server/data/migrations/utils.js +33 -1
- package/core/server/data/migrations/versions/4.42/2022-03-21-17-17-add.js +5 -0
- package/core/server/data/migrations/versions/4.43/2022-03-28-19-26-recreate-newsletter-table.js +29 -0
- package/core/server/data/migrations/versions/4.43/2022-03-29-14-45-add-members-newsletters-table.js +7 -0
- package/core/server/data/migrations/versions/4.43/2022-04-01-10-13-add-post-newsletter-relation.js +108 -0
- package/core/server/data/migrations/versions/4.43/2022-04-06-09-47-add-type-column-to-paid-subscription-events.js +7 -0
- package/core/server/data/migrations/versions/4.43/2022-04-06-14-56-add-email-newsletter-relation.js +8 -0
- package/core/server/data/migrations/versions/4.43/2022-04-08-10-45-add-subscription-id-to-mrr-events.js +7 -0
- package/core/server/data/schema/commands.js +6 -1
- package/core/server/data/schema/index.js +0 -1
- package/core/server/data/schema/schema.js +34 -16
- package/core/server/models/base/bookshelf.js +1 -1
- package/core/server/models/base/plugins/crud.js +8 -0
- package/core/server/models/member.js +18 -1
- package/core/server/models/newsletter.js +35 -1
- package/core/server/models/post.js +4 -1
- package/core/server/services/auth/setup.js +4 -1
- package/core/server/services/members/api.js +3 -1
- package/core/server/services/members/middleware.js +13 -3
- package/core/server/services/members/utils.js +13 -1
- package/core/server/services/newsletters/index.js +10 -0
- package/core/server/services/newsletters/service.js +24 -0
- package/core/server/services/slack.js +11 -3
- package/core/server/services/stats/lib/members-stats-service.js +30 -34
- package/core/server/services/stats/lib/mrr-stats-service.js +154 -0
- package/core/server/services/stats/service.js +3 -1
- package/core/server/services/stripe/service.js +1 -0
- package/core/server/web/admin/views/default-prod.html +3 -3
- package/core/server/web/admin/views/default.html +3 -3
- package/core/server/web/api/canary/admin/routes.js +1 -0
- package/core/shared/config/defaults.json +2 -2
- package/package.json +34 -34
- package/yarn.lock +332 -329
- package/content/themes/casper/assets/css/csscomb.json +0 -240
- package/core/built/assets/ghost-dark-97613c037232aba4490489431ce170ca.css +0 -1
- package/core/built/assets/ghost.min-c08ce1872f0e09edb63eb13c43606d18.css +0 -1
package/core/server/data/migrations/versions/4.43/2022-04-01-10-13-add-post-newsletter-relation.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
const DatabaseInfo = require('@tryghost/database-info');
|
|
3
|
+
const commands = require('../../../schema/commands');
|
|
4
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
5
|
+
|
|
6
|
+
const table = 'posts';
|
|
7
|
+
const column = 'newsletter_id';
|
|
8
|
+
const targetTable = 'newsletters';
|
|
9
|
+
const targetColumn = 'id';
|
|
10
|
+
|
|
11
|
+
const columnDefinition = {
|
|
12
|
+
type: 'string',
|
|
13
|
+
maxlength: 24,
|
|
14
|
+
nullable: true,
|
|
15
|
+
references: `${targetTable}.${targetColumn}`
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* This migration is adding a new column `newsletter_id` to the table posts
|
|
20
|
+
* that is a foreign key to `newsletters.id`.
|
|
21
|
+
*
|
|
22
|
+
* It isn't using the existing utils because of a performance issue. In MySQL,
|
|
23
|
+
* adding a new row without `algorithm=copy` uses the INPLACE algorithm which
|
|
24
|
+
* was too slow on big `posts` tables (~3 minutes for 10k posts). Switching to
|
|
25
|
+
* the COPY algorithm fixed the issue (~3 seconds for 10k posts).
|
|
26
|
+
*/
|
|
27
|
+
module.exports = createTransactionalMigration(
|
|
28
|
+
async function up(knex) {
|
|
29
|
+
const hasColumn = await knex.schema.hasColumn(table, column);
|
|
30
|
+
|
|
31
|
+
if (hasColumn) {
|
|
32
|
+
logging.info(`Adding ${table}.${column} column - skipping as table is correct`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
logging.info(`Adding ${table}.${column} column`);
|
|
37
|
+
|
|
38
|
+
// Use the default flow for SQLite because .toSQL() is tricky with SQLite
|
|
39
|
+
if (DatabaseInfo.isSQLite(knex)) {
|
|
40
|
+
await commands.addColumn(table, column, knex, columnDefinition);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Add the column
|
|
45
|
+
|
|
46
|
+
let sql = knex.schema.table(table, function (t) {
|
|
47
|
+
t.string(column, 24);
|
|
48
|
+
}).toSQL()[0].sql;
|
|
49
|
+
|
|
50
|
+
if (DatabaseInfo.isMySQL(knex)) {
|
|
51
|
+
// Guard against an ending semicolon
|
|
52
|
+
sql = sql.replace(/;\s*$/, '') + ', algorithm=copy';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await knex.raw(sql);
|
|
56
|
+
|
|
57
|
+
// Add the foreign key constraint
|
|
58
|
+
|
|
59
|
+
await commands.addForeign({
|
|
60
|
+
fromTable: table,
|
|
61
|
+
fromColumn: column,
|
|
62
|
+
toTable: targetTable,
|
|
63
|
+
toColumn: targetColumn,
|
|
64
|
+
cascadeDelete: false,
|
|
65
|
+
transaction: knex
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
async function down(knex) {
|
|
69
|
+
const hasColumn = await knex.schema.hasColumn(table, column);
|
|
70
|
+
|
|
71
|
+
if (!hasColumn) {
|
|
72
|
+
logging.info(`Removing ${table}.${column} column - skipping as table is correct`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
logging.info(`Removing ${table}.${column} column`);
|
|
77
|
+
|
|
78
|
+
// Use the default flow for SQLite because .toSQL() is tricky with SQLite
|
|
79
|
+
if (DatabaseInfo.isSQLite(knex)) {
|
|
80
|
+
await commands.dropColumn(table, column, knex, columnDefinition);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Drop the foreign key constraint
|
|
85
|
+
|
|
86
|
+
await commands.dropForeign({
|
|
87
|
+
fromTable: table,
|
|
88
|
+
fromColumn: column,
|
|
89
|
+
toTable: targetTable,
|
|
90
|
+
toColumn: targetColumn,
|
|
91
|
+
transaction: knex
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Drop the column
|
|
95
|
+
|
|
96
|
+
let sql = knex.schema.table(table, function (t) {
|
|
97
|
+
t.dropColumn(column);
|
|
98
|
+
}).toSQL()[0].sql;
|
|
99
|
+
|
|
100
|
+
if (DatabaseInfo.isMySQL(knex)) {
|
|
101
|
+
// Guard against an ending semicolon
|
|
102
|
+
sql = sql.replace(/;\s*$/, '') + ', algorithm=copy';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await knex.raw(sql);
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
|
|
@@ -65,7 +65,12 @@ function addColumn(tableName, column, transaction = db.knex, columnSpec) {
|
|
|
65
65
|
});
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
function dropColumn(tableName, column, transaction = db.knex) {
|
|
68
|
+
async function dropColumn(tableName, column, transaction = db.knex, columnSpec = {}) {
|
|
69
|
+
if (Object.prototype.hasOwnProperty.call(columnSpec, 'references')) {
|
|
70
|
+
const [toTable, toColumn] = columnSpec.references.split('.');
|
|
71
|
+
await dropForeign({fromTable: tableName, fromColumn: column, toTable, toColumn, transaction});
|
|
72
|
+
}
|
|
73
|
+
|
|
69
74
|
return transaction.schema.table(tableName, function (table) {
|
|
70
75
|
table.dropColumn(column);
|
|
71
76
|
});
|
|
@@ -8,6 +8,33 @@
|
|
|
8
8
|
* Long text = length 1,000,000,000
|
|
9
9
|
*/
|
|
10
10
|
module.exports = {
|
|
11
|
+
newsletters: {
|
|
12
|
+
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
13
|
+
name: {type: 'string', maxlength: 191, nullable: false, unique: true},
|
|
14
|
+
description: {type: 'string', maxlength: 2000, nullable: true},
|
|
15
|
+
slug: {type: 'string', maxlength: 191, nullable: false, unique: true},
|
|
16
|
+
sender_name: {type: 'string', maxlength: 191, nullable: false},
|
|
17
|
+
sender_email: {type: 'string', maxlength: 191, nullable: true},
|
|
18
|
+
sender_reply_to: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'newsletter', validations: {isIn: ['newsletter', 'support']}},
|
|
19
|
+
status: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'active'},
|
|
20
|
+
visibility: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
maxlength: 50,
|
|
23
|
+
nullable: false,
|
|
24
|
+
defaultTo: 'members'
|
|
25
|
+
},
|
|
26
|
+
subscribe_on_signup: {type: 'bool', nullable: false, defaultTo: true},
|
|
27
|
+
sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0},
|
|
28
|
+
header_image: {type: 'string', maxlength: 2000, nullable: true},
|
|
29
|
+
show_header_icon: {type: 'bool', nullable: false, defaultTo: true},
|
|
30
|
+
show_header_title: {type: 'bool', nullable: false, defaultTo: true},
|
|
31
|
+
title_font_category: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'sans_serif', validations: {isIn: ['serif', 'sans_serif']}},
|
|
32
|
+
title_alignment: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'center', validations: {isIn: ['center', 'left']}},
|
|
33
|
+
show_feature_image: {type: 'bool', nullable: false, defaultTo: true},
|
|
34
|
+
body_font_category: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'sans_serif', validations: {isIn: ['serif', 'sans_serif']}},
|
|
35
|
+
footer_content: {type: 'text', maxlength: 1000000000, nullable: true},
|
|
36
|
+
show_badge: {type: 'bool', nullable: false, defaultTo: true}
|
|
37
|
+
},
|
|
11
38
|
posts: {
|
|
12
39
|
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
13
40
|
uuid: {type: 'string', maxlength: 36, nullable: false, validations: {isUUID: true}},
|
|
@@ -56,6 +83,7 @@ module.exports = {
|
|
|
56
83
|
codeinjection_foot: {type: 'text', maxlength: 65535, nullable: true},
|
|
57
84
|
custom_template: {type: 'string', maxlength: 100, nullable: true},
|
|
58
85
|
canonical_url: {type: 'text', maxlength: 2000, nullable: true},
|
|
86
|
+
newsletter_id: {type: 'string', maxlength: 24, nullable: true, references: 'newsletters.id'},
|
|
59
87
|
'@@UNIQUE_CONSTRAINTS@@': [
|
|
60
88
|
['slug', 'type']
|
|
61
89
|
]
|
|
@@ -492,7 +520,9 @@ module.exports = {
|
|
|
492
520
|
},
|
|
493
521
|
members_paid_subscription_events: {
|
|
494
522
|
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
523
|
+
type: {type: 'string', maxlength: 50, nullable: true},
|
|
495
524
|
member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
|
|
525
|
+
subscription_id: {type: 'string', maxlength: 24, nullable: true},
|
|
496
526
|
from_plan: {type: 'string', maxlength: 255, nullable: true},
|
|
497
527
|
to_plan: {type: 'string', maxlength: 255, nullable: true},
|
|
498
528
|
currency: {type: 'string', maxLength: 3, nullable: false},
|
|
@@ -630,6 +660,7 @@ module.exports = {
|
|
|
630
660
|
plaintext: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
|
|
631
661
|
track_opens: {type: 'bool', nullable: false, defaultTo: false},
|
|
632
662
|
submitted_at: {type: 'dateTime', nullable: false},
|
|
663
|
+
newsletter_id: {type: 'string', maxlength: 24, nullable: true, references: 'newsletters.id'},
|
|
633
664
|
created_at: {type: 'dateTime', nullable: false},
|
|
634
665
|
created_by: {type: 'string', maxlength: 24, nullable: false},
|
|
635
666
|
updated_at: {type: 'dateTime', nullable: true},
|
|
@@ -712,22 +743,9 @@ module.exports = {
|
|
|
712
743
|
},
|
|
713
744
|
value: {type: 'text', maxlength: 65535, nullable: true}
|
|
714
745
|
},
|
|
715
|
-
|
|
746
|
+
members_newsletters: {
|
|
716
747
|
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
sender_name: {type: 'string', maxlength: 191, nullable: false},
|
|
720
|
-
sender_email: {type: 'string', maxlength: 191, nullable: false, validations: {isEmail: true}},
|
|
721
|
-
sender_reply_to: {type: 'string', maxlength: 191, nullable: false, validations: {isEmail: true}},
|
|
722
|
-
default: {type: 'bool', nullable: false, defaultTo: false},
|
|
723
|
-
status: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'active'},
|
|
724
|
-
recipient_filter: {
|
|
725
|
-
type: 'text',
|
|
726
|
-
maxlength: 1000000000,
|
|
727
|
-
nullable: false,
|
|
728
|
-
defaultTo: ''
|
|
729
|
-
},
|
|
730
|
-
subscribe_on_signup: {type: 'bool', nullable: false, defaultTo: false},
|
|
731
|
-
sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0}
|
|
748
|
+
member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
|
|
749
|
+
newsletter_id: {type: 'string', maxlength: 24, nullable: false, references: 'newsletters.id', cascadeDelete: true}
|
|
732
750
|
}
|
|
733
751
|
};
|
|
@@ -78,7 +78,7 @@ ghostBookshelf.plugin('bookshelf-relations', {
|
|
|
78
78
|
};
|
|
79
79
|
|
|
80
80
|
// CASE: disable after hook for specific relations
|
|
81
|
-
if (['permissions_roles'].indexOf(existing.relatedData.joinTableName) !== -1) {
|
|
81
|
+
if (['permissions_roles', 'members_newsletters'].indexOf(existing.relatedData.joinTableName) !== -1) {
|
|
82
82
|
return Promise.resolve();
|
|
83
83
|
}
|
|
84
84
|
|
|
@@ -137,6 +137,10 @@ module.exports = function (Bookshelf) {
|
|
|
137
137
|
options.columns = _.intersection(options.columns, this.prototype.permittedAttributes());
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
if (options.transacting && options.forUpdate) {
|
|
141
|
+
options.lock = 'forUpdate';
|
|
142
|
+
}
|
|
143
|
+
|
|
140
144
|
return model.fetch(options)
|
|
141
145
|
.catch((err) => {
|
|
142
146
|
// CASE: SQL syntax is incorrect
|
|
@@ -179,6 +183,10 @@ module.exports = function (Bookshelf) {
|
|
|
179
183
|
model.hasTimestamps = false;
|
|
180
184
|
}
|
|
181
185
|
|
|
186
|
+
if (options.transacting) {
|
|
187
|
+
options.lock = 'forUpdate';
|
|
188
|
+
}
|
|
189
|
+
|
|
182
190
|
const object = await model.fetch(options);
|
|
183
191
|
if (object) {
|
|
184
192
|
options.method = 'update';
|
|
@@ -49,6 +49,13 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
49
49
|
joinFrom: 'member_id',
|
|
50
50
|
joinTo: 'product_id'
|
|
51
51
|
},
|
|
52
|
+
newsletters: {
|
|
53
|
+
tableName: 'newsletters',
|
|
54
|
+
type: 'manyToMany',
|
|
55
|
+
joinTable: 'members_newsletters',
|
|
56
|
+
joinFrom: 'member_id',
|
|
57
|
+
joinTo: 'newsletter_id'
|
|
58
|
+
},
|
|
52
59
|
subscriptions: {
|
|
53
60
|
tableName: 'members_stripe_customers_subscriptions',
|
|
54
61
|
tableNameAs: 'subscriptions',
|
|
@@ -61,7 +68,7 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
61
68
|
};
|
|
62
69
|
},
|
|
63
70
|
|
|
64
|
-
relationships: ['products', 'labels', 'stripeCustomers', 'email_recipients'],
|
|
71
|
+
relationships: ['products', 'labels', 'stripeCustomers', 'email_recipients', 'newsletters'],
|
|
65
72
|
|
|
66
73
|
// do not delete email_recipients records when a member is destroyed. Recipient
|
|
67
74
|
// records are used for analytics and historical records
|
|
@@ -73,6 +80,7 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
73
80
|
|
|
74
81
|
relationshipBelongsTo: {
|
|
75
82
|
products: 'products',
|
|
83
|
+
newsletters: 'newsletters',
|
|
76
84
|
labels: 'labels',
|
|
77
85
|
stripeCustomers: 'members_stripe_customers',
|
|
78
86
|
email_recipients: 'email_recipients'
|
|
@@ -94,6 +102,15 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
94
102
|
});
|
|
95
103
|
},
|
|
96
104
|
|
|
105
|
+
newsletters() {
|
|
106
|
+
return this.belongsToMany('Newsletter', 'members_newsletters', 'member_id', 'newsletter_id')
|
|
107
|
+
.query((qb) => {
|
|
108
|
+
// avoids bookshelf adding a `DISTINCT` to the query
|
|
109
|
+
// we know the result set will already be unique and DISTINCT hurts query performance
|
|
110
|
+
qb.columns('newsletters.*');
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
|
|
97
114
|
offerRedemptions() {
|
|
98
115
|
return this.hasMany('OfferRedemption', 'member_id', 'id')
|
|
99
116
|
.query('orderBy', 'created_at', 'DESC');
|
|
@@ -1,7 +1,41 @@
|
|
|
1
1
|
const ghostBookshelf = require('./base');
|
|
2
2
|
|
|
3
3
|
const Newsletter = ghostBookshelf.Model.extend({
|
|
4
|
-
tableName: 'newsletters'
|
|
4
|
+
tableName: 'newsletters',
|
|
5
|
+
|
|
6
|
+
defaults: {
|
|
7
|
+
sender_reply_to: 'newsletter',
|
|
8
|
+
status: 'active',
|
|
9
|
+
visibility: 'members',
|
|
10
|
+
subscribe_on_signup: true,
|
|
11
|
+
sort_order: 0,
|
|
12
|
+
title_font_category: 'sans_serif',
|
|
13
|
+
title_alignment: 'center',
|
|
14
|
+
show_feature_image: true,
|
|
15
|
+
body_font_category: 'sans_serif',
|
|
16
|
+
show_badge: true
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
async onSaving(model, _attr, options) {
|
|
20
|
+
ghostBookshelf.Model.prototype.onSaving.apply(this, arguments);
|
|
21
|
+
|
|
22
|
+
if (model.get('name')) {
|
|
23
|
+
model.set('name', model.get('name').trim());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (model.hasChanged('slug') || !model.get('slug')) {
|
|
27
|
+
const slug = model.get('slug') || model.get('name');
|
|
28
|
+
|
|
29
|
+
if (slug) {
|
|
30
|
+
const cleanSlug = await ghostBookshelf.Model.generateSlug(Newsletter, slug, {
|
|
31
|
+
transacting: options.transacting
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
model.set({slug: cleanSlug});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
5
39
|
});
|
|
6
40
|
|
|
7
41
|
module.exports = {
|
|
@@ -557,7 +557,7 @@ Post = ghostBookshelf.Model.extend({
|
|
|
557
557
|
if (!tag.id && !tag.tag_id && tag.slug) {
|
|
558
558
|
// Clean up the provided slugs before we do any matching with existing tags
|
|
559
559
|
tag.slug = await ghostBookshelf.Model.generateSlug(
|
|
560
|
-
Tag,
|
|
560
|
+
Tag,
|
|
561
561
|
tag.slug,
|
|
562
562
|
{skipDuplicateChecks: true}
|
|
563
563
|
);
|
|
@@ -878,6 +878,9 @@ Post = ghostBookshelf.Model.extend({
|
|
|
878
878
|
// CASE: never expose the revisions
|
|
879
879
|
delete attrs.mobiledoc_revisions;
|
|
880
880
|
|
|
881
|
+
// CASE: hide the newsletter_id for now
|
|
882
|
+
delete attrs.newsletter_id;
|
|
883
|
+
|
|
881
884
|
// If the current column settings allow it...
|
|
882
885
|
if (!options.columns || (options.columns && options.columns.indexOf('primary_tag') > -1)) {
|
|
883
886
|
// ... attach a computed property of primary_tag which is the first tag if it is public, else null
|
|
@@ -142,7 +142,10 @@ async function doFixtures(data) {
|
|
|
142
142
|
mobiledoc = mobiledoc.replace(/{{date}}/, date);
|
|
143
143
|
|
|
144
144
|
const post = await models.Post.findOne({slug: key});
|
|
145
|
-
|
|
145
|
+
|
|
146
|
+
if (post) {
|
|
147
|
+
await models.Post.edit({mobiledoc}, {id: post.id});
|
|
148
|
+
}
|
|
146
149
|
});
|
|
147
150
|
|
|
148
151
|
return data;
|
|
@@ -13,6 +13,7 @@ const SingleUseTokenProvider = require('./SingleUseTokenProvider');
|
|
|
13
13
|
const urlUtils = require('../../../shared/url-utils');
|
|
14
14
|
const labsService = require('../../../shared/labs');
|
|
15
15
|
const offersService = require('../offers');
|
|
16
|
+
const getNewslettersServiceInstance = require('../newsletters');
|
|
16
17
|
|
|
17
18
|
const MAGIC_LINK_TOKEN_VALIDITY = 24 * 60 * 60 * 1000;
|
|
18
19
|
|
|
@@ -195,7 +196,8 @@ function createApiInstance(config) {
|
|
|
195
196
|
},
|
|
196
197
|
stripeAPIService: stripeService.api,
|
|
197
198
|
offersAPI: offersService.api,
|
|
198
|
-
labsService: labsService
|
|
199
|
+
labsService: labsService,
|
|
200
|
+
newslettersService: getNewslettersServiceInstance({NewsletterModel: models.Newsletter})
|
|
199
201
|
});
|
|
200
202
|
|
|
201
203
|
return membersApiInstance;
|
|
@@ -70,12 +70,12 @@ const getOfferData = async function (req, res) {
|
|
|
70
70
|
|
|
71
71
|
const updateMemberData = async function (req, res) {
|
|
72
72
|
try {
|
|
73
|
-
const data = _.pick(req.body, 'name', 'subscribed');
|
|
73
|
+
const data = _.pick(req.body, 'name', 'subscribed', 'newsletters');
|
|
74
74
|
const member = await membersService.ssr.getMemberDataFromSession(req, res);
|
|
75
75
|
if (member) {
|
|
76
76
|
const options = {
|
|
77
77
|
id: member.id,
|
|
78
|
-
withRelated: ['stripeSubscriptions', 'stripeSubscriptions.customer', 'stripeSubscriptions.stripePrice']
|
|
78
|
+
withRelated: ['stripeSubscriptions', 'stripeSubscriptions.customer', 'stripeSubscriptions.stripePrice', 'newsletters']
|
|
79
79
|
};
|
|
80
80
|
const updatedMember = await membersService.api.members.update(data, options);
|
|
81
81
|
|
|
@@ -133,6 +133,11 @@ const getPortalProductPrices = async function () {
|
|
|
133
133
|
};
|
|
134
134
|
};
|
|
135
135
|
|
|
136
|
+
const getSiteNewsletters = async function () {
|
|
137
|
+
const newsletters = await models.Newsletter.findAll();
|
|
138
|
+
return newsletters.toJSON();
|
|
139
|
+
};
|
|
140
|
+
|
|
136
141
|
const getMemberSiteData = async function (req, res) {
|
|
137
142
|
const isStripeConfigured = membersService.config.isStripeConnected();
|
|
138
143
|
const domain = urlUtils.urlFor('home', true).match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i'));
|
|
@@ -144,7 +149,7 @@ const getMemberSiteData = async function (req, res) {
|
|
|
144
149
|
}
|
|
145
150
|
const {products = [], prices = []} = await getPortalProductPrices() || {};
|
|
146
151
|
const portalVersion = config.get('portal:version');
|
|
147
|
-
|
|
152
|
+
const newsletters = await getSiteNewsletters();
|
|
148
153
|
const response = {
|
|
149
154
|
title: settingsCache.get('title'),
|
|
150
155
|
description: settingsCache.get('description'),
|
|
@@ -170,6 +175,11 @@ const getMemberSiteData = async function (req, res) {
|
|
|
170
175
|
prices,
|
|
171
176
|
products
|
|
172
177
|
};
|
|
178
|
+
|
|
179
|
+
if (labsService.isSet('multipleNewsletters')) {
|
|
180
|
+
response.newsletters = newsletters;
|
|
181
|
+
}
|
|
182
|
+
|
|
173
183
|
if (labsService.isSet('multipleProducts')) {
|
|
174
184
|
response.portal_products = settingsCache.get('portal_products');
|
|
175
185
|
}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
const labsService = require('../../../shared/labs');
|
|
2
|
+
|
|
3
|
+
function formatNewsletterResponse(newsletters) {
|
|
4
|
+
return newsletters.map(({id, name, description, sort_order: sortOrder}) => {
|
|
5
|
+
return {id, name, description, sort_order: sortOrder};
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
|
|
1
9
|
module.exports.formattedMemberResponse = function formattedMemberResponse(member) {
|
|
2
10
|
if (!member) {
|
|
3
11
|
return null;
|
|
4
12
|
}
|
|
5
|
-
|
|
13
|
+
const data = {
|
|
6
14
|
uuid: member.uuid,
|
|
7
15
|
email: member.email,
|
|
8
16
|
name: member.name,
|
|
@@ -12,4 +20,8 @@ module.exports.formattedMemberResponse = function formattedMemberResponse(member
|
|
|
12
20
|
subscriptions: member.subscriptions || [],
|
|
13
21
|
paid: member.status !== 'free'
|
|
14
22
|
};
|
|
23
|
+
if (member.newsletters && labsService.isSet('multipleNewsletters')) {
|
|
24
|
+
data.newsletters = formatNewsletterResponse(member.newsletters);
|
|
25
|
+
}
|
|
26
|
+
return data;
|
|
15
27
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const NewslettersService = require('./service.js');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @returns {NewslettersService} instance of the NewslettersService
|
|
5
|
+
*/
|
|
6
|
+
const getNewslettersServiceInstance = ({NewsletterModel}) => {
|
|
7
|
+
return new NewslettersService({NewsletterModel});
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
module.exports = getNewslettersServiceInstance;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
class NewslettersService {
|
|
2
|
+
/**
|
|
3
|
+
*
|
|
4
|
+
* @param {Object} options
|
|
5
|
+
* @param {Object} options.NewsletterModel
|
|
6
|
+
*/
|
|
7
|
+
constructor({NewsletterModel}) {
|
|
8
|
+
this.NewsletterModel = NewsletterModel;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} options browse options
|
|
14
|
+
* @returns
|
|
15
|
+
*/
|
|
16
|
+
async browse(options) {
|
|
17
|
+
let newsletters = await this.NewsletterModel.findAll(options);
|
|
18
|
+
|
|
19
|
+
return newsletters.toJSON();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = NewslettersService;
|
|
24
|
+
|
|
@@ -6,7 +6,6 @@ const {blogIcon} = require('../lib/image');
|
|
|
6
6
|
const urlUtils = require('../../shared/url-utils');
|
|
7
7
|
const urlService = require('./url');
|
|
8
8
|
const settingsCache = require('../../shared/settings-cache');
|
|
9
|
-
const schema = require('../data/schema').checks;
|
|
10
9
|
const moment = require('moment');
|
|
11
10
|
|
|
12
11
|
// Used to receive post.published model event, but also the slack.test event from the API which iirc this was done to avoid circular deps a long time ago
|
|
@@ -37,6 +36,15 @@ function getSlackSettings() {
|
|
|
37
36
|
};
|
|
38
37
|
}
|
|
39
38
|
|
|
39
|
+
/**
|
|
40
|
+
* @TODO: change this function to check for the properties we depend on
|
|
41
|
+
* @param {Object} data
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
function hasPostProperties(data) {
|
|
45
|
+
return Object.prototype.hasOwnProperty.call(data, 'html') && Object.prototype.hasOwnProperty.call(data, 'title') && Object.prototype.hasOwnProperty.call(data, 'slug');
|
|
46
|
+
}
|
|
47
|
+
|
|
40
48
|
function ping(post) {
|
|
41
49
|
let message;
|
|
42
50
|
let title;
|
|
@@ -47,7 +55,7 @@ function ping(post) {
|
|
|
47
55
|
let blogTitle = settingsCache.get('title');
|
|
48
56
|
|
|
49
57
|
// If this is a post, we want to send the link of the post
|
|
50
|
-
if (
|
|
58
|
+
if (hasPostProperties(post)) {
|
|
51
59
|
message = urlService.getUrlByResourceId(post.id, {absolute: true});
|
|
52
60
|
title = post.title ? post.title : null;
|
|
53
61
|
author = post.authors ? post.authors[0] : null;
|
|
@@ -79,7 +87,7 @@ function ping(post) {
|
|
|
79
87
|
return;
|
|
80
88
|
}
|
|
81
89
|
|
|
82
|
-
if (
|
|
90
|
+
if (hasPostProperties(post)) {
|
|
83
91
|
slackData = {
|
|
84
92
|
// We are handling the case of test notification here by checking
|
|
85
93
|
// if it is a post or a test message to check webhook working.
|