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.
Files changed (147) hide show
  1. package/content/themes/casper/package.json +2 -3
  2. package/content/themes/casper/partials/post-card.hbs +1 -1
  3. package/core/built/assets/ghost-dark-1933079797e24ccb8839657020830be5.css +1 -0
  4. package/core/built/assets/{ghost.min-1abf114ca26a71e8e1f09054f3592614.js → ghost.min-2a278873d60d6a13a4c05a396e5bed5e.js} +533 -398
  5. package/core/built/assets/ghost.min-38f3c38c0c6a1864f57079b068a0b0ce.css +1 -0
  6. package/core/built/assets/{vendor.min-9094db77ba3190cb10876f8e42e1d90d.js → vendor.min-21f79c68a284acb1b70039f3f63e5507.js} +68 -68
  7. package/core/built/assets/{vendor.min-2c8ad32b7960bb605ebc20097fee5ebd.css → vendor.min-ba66b98f7c24fa40e061c7ffc94f4e23.css} +214 -0
  8. package/core/frontend/apps/amp/lib/helpers/amp_analytics.js +1 -1
  9. package/core/frontend/apps/amp/lib/helpers/amp_components.js +1 -1
  10. package/core/frontend/apps/amp/lib/helpers/amp_content.js +1 -1
  11. package/core/frontend/apps/amp/lib/helpers/amp_style.js +1 -1
  12. package/core/frontend/apps/amp/lib/router.js +6 -5
  13. package/core/frontend/apps/private-blogging/lib/helpers/input_password.js +1 -1
  14. package/core/frontend/apps/private-blogging/lib/router.js +2 -2
  15. package/core/frontend/helpers/asset.js +1 -1
  16. package/core/frontend/helpers/author.js +1 -1
  17. package/core/frontend/helpers/authors.js +1 -1
  18. package/core/frontend/helpers/body_class.js +1 -1
  19. package/core/frontend/helpers/cancel_link.js +1 -1
  20. package/core/frontend/helpers/concat.js +1 -1
  21. package/core/frontend/helpers/content.js +1 -1
  22. package/core/frontend/helpers/date.js +1 -1
  23. package/core/frontend/helpers/encode.js +1 -1
  24. package/core/frontend/helpers/excerpt.js +1 -1
  25. package/core/frontend/helpers/facebook_url.js +1 -1
  26. package/core/frontend/helpers/foreach.js +2 -2
  27. package/core/frontend/helpers/get.js +1 -1
  28. package/core/frontend/helpers/ghost_foot.js +1 -1
  29. package/core/frontend/helpers/ghost_head.js +1 -1
  30. package/core/frontend/helpers/lang.js +1 -1
  31. package/core/frontend/helpers/link.js +1 -1
  32. package/core/frontend/helpers/link_class.js +1 -1
  33. package/core/frontend/helpers/match.js +1 -1
  34. package/core/frontend/helpers/navigation.js +1 -1
  35. package/core/frontend/helpers/pagination.js +1 -1
  36. package/core/frontend/helpers/plural.js +1 -1
  37. package/core/frontend/helpers/post_class.js +1 -1
  38. package/core/frontend/helpers/prev_post.js +6 -5
  39. package/core/frontend/helpers/price.js +1 -0
  40. package/core/frontend/helpers/products.js +1 -1
  41. package/core/frontend/helpers/reading_time.js +2 -2
  42. package/core/frontend/helpers/t.js +1 -1
  43. package/core/frontend/helpers/tags.js +1 -1
  44. package/core/frontend/helpers/tiers.js +1 -1
  45. package/core/frontend/helpers/title.js +1 -1
  46. package/core/frontend/helpers/twitter_url.js +1 -1
  47. package/core/frontend/helpers/url.js +1 -1
  48. package/core/frontend/meta/url.js +4 -4
  49. package/core/{server/data/schema → frontend/services/data}/checks.js +4 -4
  50. package/core/frontend/services/{routing/helpers → data}/entry-lookup.js +3 -3
  51. package/core/frontend/services/{routing/helpers → data}/fetch-data.js +3 -3
  52. package/core/frontend/services/data/index.js +5 -0
  53. package/core/frontend/services/{rendering.js → handlebars.js} +2 -1
  54. package/core/frontend/services/helpers/handlebars.js +1 -1
  55. package/core/frontend/services/proxy.js +2 -4
  56. package/core/frontend/services/{routing/helpers → rendering}/context.js +0 -0
  57. package/core/frontend/services/{routing/helpers → rendering}/error.js +0 -0
  58. package/core/frontend/services/{routing/helpers → rendering}/format-response.js +1 -1
  59. package/core/frontend/services/{routing/helpers → rendering}/index.js +0 -8
  60. package/core/frontend/services/{routing/helpers → rendering}/render-entries.js +1 -1
  61. package/core/frontend/services/{routing/helpers → rendering}/render-entry.js +1 -1
  62. package/core/frontend/services/{routing/helpers → rendering}/renderer.js +1 -1
  63. package/core/frontend/services/{routing/helpers → rendering}/secure.js +0 -0
  64. package/core/frontend/services/{routing/helpers → rendering}/templates.js +2 -2
  65. package/core/frontend/services/routing/CollectionRouter.js +1 -1
  66. package/core/frontend/services/routing/controllers/channel.js +9 -9
  67. package/core/frontend/services/routing/controllers/collection.js +9 -9
  68. package/core/frontend/services/routing/controllers/email-post.js +5 -6
  69. package/core/frontend/services/routing/controllers/entry.js +6 -6
  70. package/core/frontend/services/routing/controllers/preview.js +5 -6
  71. package/core/frontend/services/routing/controllers/rss.js +4 -3
  72. package/core/frontend/services/routing/controllers/static.js +5 -5
  73. package/core/frontend/services/routing/controllers/unsubscribe.js +2 -2
  74. package/core/frontend/services/routing/index.js +0 -4
  75. package/core/frontend/web/middleware/error-handler.js +2 -2
  76. package/core/server/api/canary/email-preview.js +2 -1
  77. package/core/server/api/canary/{email.js → emails.js} +0 -0
  78. package/core/server/api/canary/index.js +9 -1
  79. package/core/server/api/canary/members.js +0 -45
  80. package/core/server/api/canary/newsletters.js +45 -0
  81. package/core/server/api/canary/stats.js +23 -0
  82. package/core/server/api/canary/utils/serializers/output/email-previews.js +7 -0
  83. package/core/server/api/canary/utils/serializers/output/index.js +2 -22
  84. package/core/server/api/canary/utils/serializers/output/mappers/index.js +1 -0
  85. package/core/server/api/canary/utils/serializers/output/mappers/snippets.js +36 -0
  86. package/core/server/api/canary/utils/serializers/output/members.js +5 -2
  87. package/core/server/api/canary/utils/serializers/output/oembed.js +2 -2
  88. package/core/server/api/canary/utils/serializers/output/redirects.js +2 -2
  89. package/core/server/api/canary/utils/serializers/output/schedules.js +2 -2
  90. package/core/server/api/canary/utils/serializers/output/slack.js +2 -2
  91. package/core/server/api/canary/utils/serializers/output/themes.js +2 -2
  92. package/core/server/api/canary/utils/serializers/output/users.js +0 -23
  93. package/core/server/api/canary/utils/validators/input/index.js +6 -0
  94. package/core/server/api/shared/http.js +52 -51
  95. package/core/server/api/shared/serializers/handle.js +25 -26
  96. package/core/server/data/exporter/table-lists.js +2 -0
  97. package/core/server/data/migrations/utils.js +34 -2
  98. package/core/server/data/migrations/versions/4.42/2022-03-21-17-17-add.js +25 -0
  99. package/core/server/data/migrations/versions/4.42/2022-03-30-15-44-add-newsletter-permissions.js +28 -0
  100. package/core/server/data/migrations/versions/4.43/2022-03-28-19-26-recreate-newsletter-table.js +29 -0
  101. package/core/server/data/migrations/versions/4.43/2022-03-29-14-45-add-members-newsletters-table.js +7 -0
  102. package/core/server/data/migrations/versions/4.43/2022-04-01-10-13-add-post-newsletter-relation.js +108 -0
  103. package/core/server/data/migrations/versions/4.43/2022-04-06-09-47-add-type-column-to-paid-subscription-events.js +7 -0
  104. package/core/server/data/migrations/versions/4.43/2022-04-06-14-56-add-email-newsletter-relation.js +8 -0
  105. package/core/server/data/migrations/versions/4.43/2022-04-08-10-45-add-subscription-id-to-mrr-events.js +7 -0
  106. package/core/server/data/schema/commands.js +19 -14
  107. package/core/server/data/schema/index.js +0 -1
  108. package/core/server/data/schema/schema.js +36 -0
  109. package/core/server/models/base/bookshelf.js +1 -1
  110. package/core/server/models/base/plugins/crud.js +8 -0
  111. package/core/server/models/member.js +18 -1
  112. package/core/server/models/newsletter.js +43 -0
  113. package/core/server/models/post.js +4 -1
  114. package/core/server/services/auth/setup.js +4 -1
  115. package/core/server/services/mega/template.js +25 -13
  116. package/core/server/services/members/api.js +3 -1
  117. package/core/server/services/members/middleware.js +13 -3
  118. package/core/server/services/members/service.js +2 -1
  119. package/core/server/services/members/utils.js +13 -1
  120. package/core/server/services/newsletters/index.js +10 -0
  121. package/core/server/services/newsletters/service.js +24 -0
  122. package/core/server/services/slack.js +11 -3
  123. package/core/server/services/stats/index.js +1 -0
  124. package/core/server/services/stats/lib/members-stats-service.js +161 -0
  125. package/core/server/services/stats/lib/mrr-stats-service.js +154 -0
  126. package/core/server/services/stats/service.js +8 -0
  127. package/core/server/services/stripe/service.js +1 -0
  128. package/core/server/services/webhooks/webhooks-service.js +3 -1
  129. package/core/server/web/admin/views/default-prod.html +5 -5
  130. package/core/server/web/admin/views/default.html +5 -5
  131. package/core/server/web/api/canary/admin/routes.js +9 -2
  132. package/core/shared/config/defaults.json +2 -2
  133. package/core/shared/config/env/config.development.json +26 -0
  134. package/core/shared/config/env/config.production.json +21 -0
  135. package/core/shared/config/env/config.testing-mysql.json +59 -0
  136. package/core/shared/config/env/config.testing.json +58 -0
  137. package/package.json +50 -50
  138. package/yarn.lock +700 -769
  139. package/content/themes/casper/assets/css/csscomb.json +0 -240
  140. package/core/built/assets/ghost-dark-146c4c688b47d45c4aa018ee0f79cebc.css +0 -1
  141. package/core/built/assets/ghost.min-a73b150c7eecc4641d377cc73fb5eecd.css +0 -1
  142. package/core/server/api/canary/utils/serializers/output/email-preview.js +0 -10
  143. package/core/server/api/canary/utils/serializers/output/emails.js +0 -22
  144. package/core/server/api/canary/utils/serializers/output/identities.js +0 -7
  145. package/core/server/api/canary/utils/serializers/output/member-signin-urls.js +0 -7
  146. package/core/server/api/canary/utils/serializers/output/snippets.js +0 -107
  147. package/core/server/api/canary/utils/serializers/output/webhooks.js +0 -15
@@ -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
+ });
@@ -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,7 @@
1
+ const {createAddColumnMigration} = require('../../utils');
2
+
3
+ module.exports = createAddColumnMigration('members_paid_subscription_events', 'type', {
4
+ type: 'string',
5
+ maxlength: '50',
6
+ nullable: true
7
+ });
@@ -0,0 +1,8 @@
1
+ const {createAddColumnMigration} = require('../../utils');
2
+
3
+ module.exports = createAddColumnMigration('emails', 'newsletter_id', {
4
+ type: 'string',
5
+ maxlength: 24,
6
+ nullable: true,
7
+ references: 'newsletters.id'
8
+ });
@@ -0,0 +1,7 @@
1
+ const {createAddColumnMigration} = require('../../utils');
2
+
3
+ module.exports = createAddColumnMigration('members_paid_subscription_events', 'subscription_id', {
4
+ type: 'string',
5
+ maxlength: 24,
6
+ nullable: true
7
+ });
@@ -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: ${columns} in table ${tableName}`);
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: ${columns} already exists for table: ${tableName}`);
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: ${columns} already exists for table: ${tableName}`);
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: ${columns} in table: ${tableName}`);
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: ${columns} does not exist for table: ${tableName}`);
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: ${columns} does not exist for table: ${tableName}`);
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} - foreign key already exists`);
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} - foreign key already exists`);
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} - foreign key does not exist`);
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} - foreign key does not exist`);
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: ${columns} already exists for table: ${tableName}`);
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: ${columns} in table ${tableName}`);
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: ${columns} already exists for table: ${tableName}`);
299
+ logging.warn(`Primary key constraint for '${columns}' already exists for table '${tableName}'`);
295
300
  return;
296
301
  }
297
302
  throw err;
@@ -1,5 +1,4 @@
1
1
  module.exports.tables = require('./schema');
2
- module.exports.checks = require('./checks');
3
2
  module.exports.commands = require('./commands');
4
3
  module.exports.defaultSettings = require('./default-settings');
5
4
  module.exports.validate = require('./validator');
@@ -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
- await models.Post.edit({mobiledoc}, {id: post.id});
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.15em;
202
- font-weight: 600;
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: 600;
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.3em;
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.4em;
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.4em;
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: 600;
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: #23323D;
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: #23323D;
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: 600;
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;