ghost 4.42.1 → 4.44.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/content/themes/casper/package.json +2 -3
- package/content/themes/casper/partials/post-card.hbs +1 -1
- package/core/built/assets/ghost-dark-470c1ef06b10e5c40ad05f3a642eaaea.css +1 -0
- package/core/built/assets/{ghost.min-20096eef632760c3a2906e243adbd24b.js → ghost.min-1e7dce606e92a03207d15ae7eb3d3c23.js} +411 -323
- package/core/built/assets/ghost.min-d0c17e8314b5583c0df5d05fab3c051c.css +1 -0
- package/core/built/assets/{vendor.min-21f79c68a284acb1b70039f3f63e5507.js → vendor.min-fe2c9b1235b4119b5406b788db2db434.js} +88 -82
- 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/authentication.js +2 -2
- package/core/server/api/canary/posts.js +1 -0
- package/core/server/api/canary/stats.js +9 -0
- package/core/server/api/canary/utils/serializers/output/members.js +8 -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/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/schema/commands.js +6 -1
- package/core/server/data/schema/fixtures/fixtures.json +26 -1
- package/core/server/data/schema/index.js +0 -1
- package/core/server/data/schema/schema.js +36 -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 +21 -1
- package/core/server/models/newsletter.js +42 -1
- package/core/server/models/post.js +12 -2
- package/core/server/models/stripe-customer-subscription.js +4 -0
- package/core/server/services/auth/setup.js +21 -8
- package/core/server/services/mega/mega.js +3 -1
- package/core/server/services/members/api.js +3 -1
- package/core/server/services/members/middleware.js +13 -3
- package/core/server/services/members/service.js +3 -11
- 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/posts/posts-service.js +20 -1
- 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 +4 -4
- package/core/server/web/admin/views/default.html +4 -4
- package/core/server/web/api/canary/admin/routes.js +1 -0
- package/core/shared/config/defaults.json +2 -2
- package/package.json +39 -39
- package/yarn.lock +410 -369
- package/content/themes/casper/assets/css/csscomb.json +0 -240
- package/core/built/assets/ghost-dark-a93afb20027060d760ac6d78f115a76f.css +0 -1
- package/core/built/assets/ghost.min-ce35ef1b76d9a943ab912c076773b132.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
|
+
|
|
@@ -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
|
+
|
|
@@ -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
|
});
|
|
@@ -19,6 +19,15 @@
|
|
|
19
19
|
}
|
|
20
20
|
]
|
|
21
21
|
},
|
|
22
|
+
{
|
|
23
|
+
"name": "Newsletter",
|
|
24
|
+
"entries": [
|
|
25
|
+
{
|
|
26
|
+
"name": "Default Newsletter",
|
|
27
|
+
"slug": "default-newsletter"
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
},
|
|
22
31
|
{
|
|
23
32
|
"name": "Tag",
|
|
24
33
|
"entries": [
|
|
@@ -531,6 +540,21 @@
|
|
|
531
540
|
"name": "Edit custom theme settings",
|
|
532
541
|
"action_type": "edit",
|
|
533
542
|
"object_type": "custom_theme_setting"
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
"name": "Browse newsletters",
|
|
546
|
+
"action_type": "browse",
|
|
547
|
+
"object_type": "newsletter"
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
"name": "Add newsletters",
|
|
551
|
+
"action_type": "add",
|
|
552
|
+
"object_type": "newsletter"
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
"name": "Edit newsletters",
|
|
556
|
+
"action_type": "edit",
|
|
557
|
+
"object_type": "newsletter"
|
|
534
558
|
}
|
|
535
559
|
]
|
|
536
560
|
},
|
|
@@ -642,7 +666,8 @@
|
|
|
642
666
|
"custom_theme_setting": "all",
|
|
643
667
|
"offer": "all",
|
|
644
668
|
"authentication": "resetAllPasswords",
|
|
645
|
-
"members_stripe_connect": "auth"
|
|
669
|
+
"members_stripe_connect": "auth",
|
|
670
|
+
"newsletter": "all"
|
|
646
671
|
},
|
|
647
672
|
"DB Backup Integration": {
|
|
648
673
|
"db": "all"
|
|
@@ -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: true},
|
|
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},
|
|
@@ -545,6 +575,8 @@ module.exports = {
|
|
|
545
575
|
created_by: {type: 'string', maxlength: 24, nullable: false},
|
|
546
576
|
updated_at: {type: 'dateTime', nullable: true},
|
|
547
577
|
updated_by: {type: 'string', maxlength: 24, nullable: true},
|
|
578
|
+
mrr: {type: 'integer', unsigned: true, nullable: false, defaultTo: 0},
|
|
579
|
+
offer_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'offers.id'},
|
|
548
580
|
/* Below fields are now redundant as we link prie_id to stripe_prices table */
|
|
549
581
|
plan_id: {type: 'string', maxlength: 255, nullable: false, unique: false},
|
|
550
582
|
plan_nickname: {type: 'string', maxlength: 50, nullable: false},
|
|
@@ -630,6 +662,7 @@ module.exports = {
|
|
|
630
662
|
plaintext: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
|
|
631
663
|
track_opens: {type: 'bool', nullable: false, defaultTo: false},
|
|
632
664
|
submitted_at: {type: 'dateTime', nullable: false},
|
|
665
|
+
newsletter_id: {type: 'string', maxlength: 24, nullable: true, references: 'newsletters.id'},
|
|
633
666
|
created_at: {type: 'dateTime', nullable: false},
|
|
634
667
|
created_by: {type: 'string', maxlength: 24, nullable: false},
|
|
635
668
|
updated_at: {type: 'dateTime', nullable: true},
|
|
@@ -712,22 +745,9 @@ module.exports = {
|
|
|
712
745
|
},
|
|
713
746
|
value: {type: 'text', maxlength: 65535, nullable: true}
|
|
714
747
|
},
|
|
715
|
-
|
|
748
|
+
members_newsletters: {
|
|
716
749
|
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}
|
|
750
|
+
member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
|
|
751
|
+
newsletter_id: {type: 'string', maxlength: 24, nullable: false, references: 'newsletters.id', cascadeDelete: true}
|
|
732
752
|
}
|
|
733
753
|
};
|
|
@@ -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';
|
|
@@ -30,6 +30,9 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
30
30
|
}, {
|
|
31
31
|
key: 'products',
|
|
32
32
|
replacement: 'products.slug'
|
|
33
|
+
}, {
|
|
34
|
+
key: 'newsletters',
|
|
35
|
+
replacement: 'newsletters.slug'
|
|
33
36
|
}];
|
|
34
37
|
},
|
|
35
38
|
|
|
@@ -49,6 +52,13 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
49
52
|
joinFrom: 'member_id',
|
|
50
53
|
joinTo: 'product_id'
|
|
51
54
|
},
|
|
55
|
+
newsletters: {
|
|
56
|
+
tableName: 'newsletters',
|
|
57
|
+
type: 'manyToMany',
|
|
58
|
+
joinTable: 'members_newsletters',
|
|
59
|
+
joinFrom: 'member_id',
|
|
60
|
+
joinTo: 'newsletter_id'
|
|
61
|
+
},
|
|
52
62
|
subscriptions: {
|
|
53
63
|
tableName: 'members_stripe_customers_subscriptions',
|
|
54
64
|
tableNameAs: 'subscriptions',
|
|
@@ -61,7 +71,7 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
61
71
|
};
|
|
62
72
|
},
|
|
63
73
|
|
|
64
|
-
relationships: ['products', 'labels', 'stripeCustomers', 'email_recipients'],
|
|
74
|
+
relationships: ['products', 'labels', 'stripeCustomers', 'email_recipients', 'newsletters'],
|
|
65
75
|
|
|
66
76
|
// do not delete email_recipients records when a member is destroyed. Recipient
|
|
67
77
|
// records are used for analytics and historical records
|
|
@@ -73,6 +83,7 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
73
83
|
|
|
74
84
|
relationshipBelongsTo: {
|
|
75
85
|
products: 'products',
|
|
86
|
+
newsletters: 'newsletters',
|
|
76
87
|
labels: 'labels',
|
|
77
88
|
stripeCustomers: 'members_stripe_customers',
|
|
78
89
|
email_recipients: 'email_recipients'
|
|
@@ -94,6 +105,15 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
94
105
|
});
|
|
95
106
|
},
|
|
96
107
|
|
|
108
|
+
newsletters() {
|
|
109
|
+
return this.belongsToMany('Newsletter', 'members_newsletters', 'member_id', 'newsletter_id')
|
|
110
|
+
.query((qb) => {
|
|
111
|
+
// avoids bookshelf adding a `DISTINCT` to the query
|
|
112
|
+
// we know the result set will already be unique and DISTINCT hurts query performance
|
|
113
|
+
qb.columns('newsletters.*');
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
|
|
97
117
|
offerRedemptions() {
|
|
98
118
|
return this.hasMany('OfferRedemption', 'member_id', 'id')
|
|
99
119
|
.query('orderBy', 'created_at', 'DESC');
|
|
@@ -1,7 +1,48 @@
|
|
|
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
|
+
show_header_icon: true,
|
|
18
|
+
show_header_title: true
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
async onSaving(model, _attr, options) {
|
|
22
|
+
ghostBookshelf.Model.prototype.onSaving.apply(this, arguments);
|
|
23
|
+
|
|
24
|
+
if (model.get('name')) {
|
|
25
|
+
model.set('name', model.get('name').trim());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (model.hasChanged('slug') || !model.get('slug')) {
|
|
29
|
+
const slug = model.get('slug') || model.get('name');
|
|
30
|
+
|
|
31
|
+
if (slug) {
|
|
32
|
+
const cleanSlug = await ghostBookshelf.Model.generateSlug(Newsletter, slug, {
|
|
33
|
+
transacting: options.transacting
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
model.set({slug: cleanSlug});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}, {
|
|
41
|
+
orderDefaultOptions: function orderDefaultOptions() {
|
|
42
|
+
return {
|
|
43
|
+
sort_order: 'ASC'
|
|
44
|
+
};
|
|
45
|
+
}
|
|
5
46
|
});
|
|
6
47
|
|
|
7
48
|
module.exports = {
|