ghost 4.41.3 → 4.43.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-1933079797e24ccb8839657020830be5.css +1 -0
- package/core/built/assets/{ghost.min-1abf114ca26a71e8e1f09054f3592614.js → ghost.min-2a278873d60d6a13a4c05a396e5bed5e.js} +533 -398
- package/core/built/assets/ghost.min-38f3c38c0c6a1864f57079b068a0b0ce.css +1 -0
- package/core/built/assets/{vendor.min-9094db77ba3190cb10876f8e42e1d90d.js → vendor.min-21f79c68a284acb1b70039f3f63e5507.js} +68 -68
- package/core/built/assets/{vendor.min-2c8ad32b7960bb605ebc20097fee5ebd.css → vendor.min-ba66b98f7c24fa40e061c7ffc94f4e23.css} +214 -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/price.js +1 -0
- 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/email-preview.js +2 -1
- package/core/server/api/canary/{email.js → emails.js} +0 -0
- package/core/server/api/canary/index.js +9 -1
- package/core/server/api/canary/members.js +0 -45
- package/core/server/api/canary/newsletters.js +45 -0
- package/core/server/api/canary/stats.js +23 -0
- package/core/server/api/canary/utils/serializers/output/email-previews.js +7 -0
- package/core/server/api/canary/utils/serializers/output/index.js +2 -22
- package/core/server/api/canary/utils/serializers/output/mappers/index.js +1 -0
- package/core/server/api/canary/utils/serializers/output/mappers/snippets.js +36 -0
- package/core/server/api/canary/utils/serializers/output/members.js +5 -2
- package/core/server/api/canary/utils/serializers/output/oembed.js +2 -2
- package/core/server/api/canary/utils/serializers/output/redirects.js +2 -2
- package/core/server/api/canary/utils/serializers/output/schedules.js +2 -2
- package/core/server/api/canary/utils/serializers/output/slack.js +2 -2
- package/core/server/api/canary/utils/serializers/output/themes.js +2 -2
- package/core/server/api/canary/utils/serializers/output/users.js +0 -23
- package/core/server/api/canary/utils/validators/input/index.js +6 -0
- package/core/server/api/shared/http.js +52 -51
- package/core/server/api/shared/serializers/handle.js +25 -26
- package/core/server/data/exporter/table-lists.js +2 -0
- package/core/server/data/migrations/utils.js +34 -2
- package/core/server/data/migrations/versions/4.42/2022-03-21-17-17-add.js +25 -0
- package/core/server/data/migrations/versions/4.42/2022-03-30-15-44-add-newsletter-permissions.js +28 -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 +19 -14
- package/core/server/data/schema/index.js +0 -1
- package/core/server/data/schema/schema.js +36 -0
- 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 +43 -0
- package/core/server/models/post.js +4 -1
- package/core/server/services/auth/setup.js +4 -1
- package/core/server/services/mega/template.js +25 -13
- 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 +2 -1
- 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/index.js +1 -0
- package/core/server/services/stats/lib/members-stats-service.js +161 -0
- package/core/server/services/stats/lib/mrr-stats-service.js +154 -0
- package/core/server/services/stats/service.js +8 -0
- package/core/server/services/stripe/service.js +1 -0
- package/core/server/services/webhooks/webhooks-service.js +3 -1
- package/core/server/web/admin/views/default-prod.html +5 -5
- package/core/server/web/admin/views/default.html +5 -5
- package/core/server/web/api/canary/admin/routes.js +9 -2
- package/core/shared/config/defaults.json +2 -2
- package/core/shared/config/env/config.development.json +26 -0
- package/core/shared/config/env/config.production.json +21 -0
- package/core/shared/config/env/config.testing-mysql.json +59 -0
- package/core/shared/config/env/config.testing.json +58 -0
- package/package.json +50 -50
- package/yarn.lock +700 -769
- package/content/themes/casper/assets/css/csscomb.json +0 -240
- package/core/built/assets/ghost-dark-146c4c688b47d45c4aa018ee0f79cebc.css +0 -1
- package/core/built/assets/ghost.min-a73b150c7eecc4641d377cc73fb5eecd.css +0 -1
- package/core/server/api/canary/utils/serializers/output/email-preview.js +0 -10
- package/core/server/api/canary/utils/serializers/output/emails.js +0 -22
- package/core/server/api/canary/utils/serializers/output/identities.js +0 -7
- package/core/server/api/canary/utils/serializers/output/member-signin-urls.js +0 -7
- package/core/server/api/canary/utils/serializers/output/snippets.js +0 -107
- package/core/server/api/canary/utils/serializers/output/webhooks.js +0 -15
package/core/server/data/migrations/versions/4.43/2022-03-29-14-45-add-members-newsletters-table.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
const {addTable} = require('../../utils');
|
|
2
|
+
|
|
3
|
+
module.exports = addTable('members_newsletters', {
|
|
4
|
+
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
5
|
+
member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
|
|
6
|
+
newsletter_id: {type: 'string', maxlength: 24, nullable: false, references: 'newsletters.id', cascadeDelete: true}
|
|
7
|
+
});
|
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
|
});
|
|
@@ -80,18 +85,18 @@ function dropColumn(tableName, column, transaction = db.knex) {
|
|
|
80
85
|
*/
|
|
81
86
|
async function addUnique(tableName, columns, transaction = db.knex) {
|
|
82
87
|
try {
|
|
83
|
-
logging.info(`Adding unique constraint for
|
|
88
|
+
logging.info(`Adding unique constraint for '${columns}' in table '${tableName}'`);
|
|
84
89
|
|
|
85
90
|
return await transaction.schema.table(tableName, function (table) {
|
|
86
91
|
table.unique(columns);
|
|
87
92
|
});
|
|
88
93
|
} catch (err) {
|
|
89
94
|
if (err.code === 'SQLITE_ERROR') {
|
|
90
|
-
logging.warn(`Constraint for
|
|
95
|
+
logging.warn(`Constraint for '${columns}' already exists for table '${tableName}'`);
|
|
91
96
|
return;
|
|
92
97
|
}
|
|
93
98
|
if (err.code === 'ER_DUP_KEYNAME') {
|
|
94
|
-
logging.warn(`Constraint for
|
|
99
|
+
logging.warn(`Constraint for '${columns}' already exists for table '${tableName}'`);
|
|
95
100
|
return;
|
|
96
101
|
}
|
|
97
102
|
throw err;
|
|
@@ -107,18 +112,18 @@ async function addUnique(tableName, columns, transaction = db.knex) {
|
|
|
107
112
|
*/
|
|
108
113
|
async function dropUnique(tableName, columns, transaction = db.knex) {
|
|
109
114
|
try {
|
|
110
|
-
logging.info(`Dropping unique constraint for
|
|
115
|
+
logging.info(`Dropping unique constraint for '${columns}' in table '${tableName}'`);
|
|
111
116
|
|
|
112
117
|
return await transaction.schema.table(tableName, function (table) {
|
|
113
118
|
table.dropUnique(columns);
|
|
114
119
|
});
|
|
115
120
|
} catch (err) {
|
|
116
121
|
if (err.code === 'SQLITE_ERROR') {
|
|
117
|
-
logging.warn(`Constraint for
|
|
122
|
+
logging.warn(`Constraint for '${columns}' does not exist for table '${tableName}'`);
|
|
118
123
|
return;
|
|
119
124
|
}
|
|
120
125
|
if (err.code === 'ER_CANT_DROP_FIELD_OR_KEY') {
|
|
121
|
-
logging.warn(`Constraint for
|
|
126
|
+
logging.warn(`Constraint for '${columns}' does not exist for table '${tableName}'`);
|
|
122
127
|
return;
|
|
123
128
|
}
|
|
124
129
|
throw err;
|
|
@@ -164,7 +169,7 @@ async function addForeign({fromTable, fromColumn, toTable, toColumn, cascadeDele
|
|
|
164
169
|
if (DatabaseInfo.isSQLite(transaction)) {
|
|
165
170
|
const foreignKeyExists = await hasForeignSQLite({fromTable, fromColumn, toTable, toColumn, transaction});
|
|
166
171
|
if (foreignKeyExists) {
|
|
167
|
-
logging.warn(`Skipped adding foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} -
|
|
172
|
+
logging.warn(`Skipped adding foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} - already exists`);
|
|
168
173
|
return;
|
|
169
174
|
}
|
|
170
175
|
}
|
|
@@ -195,7 +200,7 @@ async function addForeign({fromTable, fromColumn, toTable, toColumn, cascadeDele
|
|
|
195
200
|
}
|
|
196
201
|
} catch (err) {
|
|
197
202
|
if (err.code === 'ER_DUP_KEY' || err.code === 'ER_FK_DUP_KEY' || err.code === 'ER_FK_DUP_NAME') {
|
|
198
|
-
logging.warn(`Skipped adding foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} -
|
|
203
|
+
logging.warn(`Skipped adding foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} - already exists`);
|
|
199
204
|
return;
|
|
200
205
|
}
|
|
201
206
|
throw err;
|
|
@@ -216,7 +221,7 @@ async function dropForeign({fromTable, fromColumn, toTable, toColumn, transactio
|
|
|
216
221
|
if (DatabaseInfo.isSQLite(transaction)) {
|
|
217
222
|
const foreignKeyExists = await hasForeignSQLite({fromTable, fromColumn, toTable, toColumn, transaction});
|
|
218
223
|
if (!foreignKeyExists) {
|
|
219
|
-
logging.warn(`Skipped dropping foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} -
|
|
224
|
+
logging.warn(`Skipped dropping foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} - does not exist`);
|
|
220
225
|
return;
|
|
221
226
|
}
|
|
222
227
|
}
|
|
@@ -243,7 +248,7 @@ async function dropForeign({fromTable, fromColumn, toTable, toColumn, transactio
|
|
|
243
248
|
}
|
|
244
249
|
} catch (err) {
|
|
245
250
|
if (err.code === 'ER_CANT_DROP_FIELD_OR_KEY') {
|
|
246
|
-
logging.warn(`Skipped dropping foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} -
|
|
251
|
+
logging.warn(`Skipped dropping foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} - does not exist`);
|
|
247
252
|
return;
|
|
248
253
|
}
|
|
249
254
|
throw err;
|
|
@@ -280,18 +285,18 @@ async function addPrimaryKey(tableName, columns, transaction = db.knex) {
|
|
|
280
285
|
if (DatabaseInfo.isSQLite(transaction)) {
|
|
281
286
|
const primaryKeyExists = await hasPrimaryKeySQLite(tableName, transaction);
|
|
282
287
|
if (primaryKeyExists) {
|
|
283
|
-
logging.warn(`Primary key constraint for
|
|
288
|
+
logging.warn(`Primary key constraint for '${columns}' already exists for table '${tableName}'`);
|
|
284
289
|
return;
|
|
285
290
|
}
|
|
286
291
|
}
|
|
287
292
|
try {
|
|
288
|
-
logging.info(`Adding primary key constraint for
|
|
293
|
+
logging.info(`Adding primary key constraint for '${columns}' in table '${tableName}'`);
|
|
289
294
|
return await transaction.schema.table(tableName, function (table) {
|
|
290
295
|
table.primary(columns);
|
|
291
296
|
});
|
|
292
297
|
} catch (err) {
|
|
293
298
|
if (err.code === 'ER_MULTIPLE_PRI_KEY') {
|
|
294
|
-
logging.warn(`Primary key constraint for
|
|
299
|
+
logging.warn(`Primary key constraint for '${columns}' already exists for table '${tableName}'`);
|
|
295
300
|
return;
|
|
296
301
|
}
|
|
297
302
|
throw err;
|
|
@@ -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},
|
|
@@ -711,5 +742,10 @@ module.exports = {
|
|
|
711
742
|
}
|
|
712
743
|
},
|
|
713
744
|
value: {type: 'text', maxlength: 65535, nullable: true}
|
|
745
|
+
},
|
|
746
|
+
members_newsletters: {
|
|
747
|
+
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
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}
|
|
714
750
|
}
|
|
715
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');
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const ghostBookshelf = require('./base');
|
|
2
|
+
|
|
3
|
+
const Newsletter = ghostBookshelf.Model.extend({
|
|
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
|
+
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
Newsletter: ghostBookshelf.model('Newsletter', Newsletter)
|
|
43
|
+
};
|
|
@@ -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;
|
|
@@ -198,48 +198,55 @@ h5,
|
|
|
198
198
|
h6 {
|
|
199
199
|
margin-top: 0;
|
|
200
200
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
|
201
|
-
line-height: 1.
|
|
202
|
-
font-weight:
|
|
201
|
+
line-height: 1.11em;
|
|
202
|
+
font-weight: 700;
|
|
203
203
|
text-rendering: optimizeLegibility;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
h1 {
|
|
207
207
|
margin: 1.5em 0 0.5em 0;
|
|
208
208
|
font-size: 42px;
|
|
209
|
-
font-weight:
|
|
209
|
+
font-weight: 700;
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
h2 {
|
|
213
213
|
margin: 1.5em 0 0.5em 0;
|
|
214
214
|
font-size: 32px;
|
|
215
|
-
line-height: 1.22em;
|
|
216
215
|
}
|
|
217
216
|
|
|
218
217
|
h3 {
|
|
219
218
|
margin: 1.5em 0 0.5em 0;
|
|
220
219
|
font-size: 26px;
|
|
221
|
-
line-height: 1.25em;
|
|
222
220
|
}
|
|
223
221
|
|
|
224
222
|
h4 {
|
|
225
223
|
margin: 1.8em 0 0.5em 0;
|
|
226
224
|
font-size: 21px;
|
|
227
|
-
line-height: 1.
|
|
225
|
+
line-height: 1.2em;
|
|
228
226
|
}
|
|
229
227
|
|
|
230
228
|
h5 {
|
|
231
229
|
margin: 2em 0 0.5em 0;
|
|
232
230
|
font-size: 19px;
|
|
233
|
-
line-height: 1.
|
|
231
|
+
line-height: 1.3em;
|
|
234
232
|
}
|
|
235
233
|
|
|
236
234
|
h6 {
|
|
237
235
|
margin: 2em 0 0.5em 0;
|
|
238
236
|
font-size: 19px;
|
|
239
|
-
line-height: 1.
|
|
237
|
+
line-height: 1.3em;
|
|
240
238
|
font-weight: 700;
|
|
241
239
|
}
|
|
242
240
|
|
|
241
|
+
h1 strong,
|
|
242
|
+
h2 strong,
|
|
243
|
+
h3 strong,
|
|
244
|
+
h4 strong,
|
|
245
|
+
h5 strong,
|
|
246
|
+
h6 strong {
|
|
247
|
+
font-weight: 800;
|
|
248
|
+
}
|
|
249
|
+
|
|
243
250
|
strong {
|
|
244
251
|
font-weight: 700;
|
|
245
252
|
}
|
|
@@ -301,6 +308,10 @@ figure blockquote p {
|
|
|
301
308
|
|
|
302
309
|
.site-info {
|
|
303
310
|
padding-top: 50px;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.site-info-bordered {
|
|
314
|
+
padding-top: 50px;
|
|
304
315
|
border-bottom: 1px solid #e5eff5;
|
|
305
316
|
}
|
|
306
317
|
|
|
@@ -322,11 +333,12 @@ figure blockquote p {
|
|
|
322
333
|
padding-bottom: 10px;
|
|
323
334
|
font-size: 42px;
|
|
324
335
|
line-height: 1.1em;
|
|
325
|
-
font-weight:
|
|
336
|
+
font-weight: 700;
|
|
326
337
|
text-align: center;
|
|
327
338
|
}
|
|
328
339
|
.post-title-serif {
|
|
329
340
|
font-family: Georgia, serif;
|
|
341
|
+
letter-spacing: -0.01em;
|
|
330
342
|
}
|
|
331
343
|
.post-title-left {
|
|
332
344
|
text-align: left;
|
|
@@ -388,7 +400,7 @@ figure blockquote p {
|
|
|
388
400
|
font-family: Georgia, serif;
|
|
389
401
|
font-size: 18px;
|
|
390
402
|
line-height: 1.5em;
|
|
391
|
-
color: #
|
|
403
|
+
color: #15212A;
|
|
392
404
|
padding-bottom: 20px;
|
|
393
405
|
border-bottom: 1px solid #e5eff5;
|
|
394
406
|
}
|
|
@@ -397,7 +409,7 @@ figure blockquote p {
|
|
|
397
409
|
max-width: 600px !important;
|
|
398
410
|
font-size: 17px;
|
|
399
411
|
line-height: 1.5em;
|
|
400
|
-
color: #
|
|
412
|
+
color: #15212A;
|
|
401
413
|
padding-bottom: 20px;
|
|
402
414
|
border-bottom: 1px solid #e5eff5;
|
|
403
415
|
}
|
|
@@ -711,7 +723,7 @@ a[data-flickr-embed] img {
|
|
|
711
723
|
}
|
|
712
724
|
|
|
713
725
|
.kg-header-card h3 strong {
|
|
714
|
-
font-weight:
|
|
726
|
+
font-weight: 700;
|
|
715
727
|
}
|
|
716
728
|
|
|
717
729
|
.kg-header-card.kg-size-large h3 {
|
|
@@ -1148,7 +1160,7 @@ ${ templateSettings.showBadge ? `
|
|
|
1148
1160
|
|
|
1149
1161
|
${ templateSettings.showHeaderIcon || templateSettings.showHeaderTitle ? `
|
|
1150
1162
|
<tr>
|
|
1151
|
-
<td class="site-info" width="100%" align="center">
|
|
1163
|
+
<td class="${templateSettings.showHeaderTitle ? `site-info-bordered` : `site-info`}" width="100%" align="center">
|
|
1152
1164
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
|
1153
1165
|
${ templateSettings.showHeaderIcon && site.iconUrl ? `
|
|
1154
1166
|
<tr>
|
|
@@ -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;
|