ghost 5.8.1 → 5.9.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 (117) hide show
  1. package/components/tryghost-adapter-manager-0.0.0.tgz +0 -0
  2. package/components/tryghost-api-framework-0.0.0.tgz +0 -0
  3. package/components/tryghost-bootstrap-socket-0.0.0.tgz +0 -0
  4. package/components/tryghost-custom-theme-settings-service-0.0.0.tgz +0 -0
  5. package/components/tryghost-email-analytics-provider-mailgun-0.0.0.tgz +0 -0
  6. package/components/tryghost-email-analytics-service-0.0.0.tgz +0 -0
  7. package/components/tryghost-job-manager-0.0.0.tgz +0 -0
  8. package/components/tryghost-mailgun-client-0.0.0.tgz +0 -0
  9. package/components/tryghost-member-analytics-service-0.0.0.tgz +0 -0
  10. package/components/tryghost-members-api-0.0.0.tgz +0 -0
  11. package/components/tryghost-members-importer-0.0.0.tgz +0 -0
  12. package/components/tryghost-members-offers-0.0.0.tgz +0 -0
  13. package/components/tryghost-members-payments-0.0.0.tgz +0 -0
  14. package/components/tryghost-members-ssr-0.0.0.tgz +0 -0
  15. package/components/tryghost-members-stripe-service-0.0.0.tgz +0 -0
  16. package/components/tryghost-minifier-0.0.0.tgz +0 -0
  17. package/components/tryghost-mw-cache-control-0.0.0.tgz +0 -0
  18. package/components/tryghost-mw-error-handler-0.0.0.tgz +0 -0
  19. package/components/tryghost-package-json-0.0.0.tgz +0 -0
  20. package/components/tryghost-session-service-0.0.0.tgz +0 -0
  21. package/components/tryghost-settings-path-manager-0.0.0.tgz +0 -0
  22. package/components/tryghost-update-check-service-0.0.0.tgz +0 -0
  23. package/content/themes/casper/README.md +1 -1
  24. package/content/themes/casper/assets/built/portal.min.js +3 -0
  25. package/content/themes/casper/assets/built/screen.css +1 -1
  26. package/content/themes/casper/assets/built/screen.css.map +1 -1
  27. package/content/themes/casper/assets/css/screen.css +119 -37
  28. package/content/themes/casper/package.json +3 -3
  29. package/content/themes/casper/partials/post-card.hbs +3 -1
  30. package/content/themes/casper/post.hbs +7 -7
  31. package/content/themes/casper/yarn.lock +69 -68
  32. package/core/boot.js +2 -0
  33. package/core/built/admin/assets/{chunk.143.904e017ad397bb681e9d.js → chunk.143.e35fdb482bc822313f0c.js} +5 -5
  34. package/core/built/admin/assets/{chunk.178.9541bdb92bded29cd60d.js → chunk.178.1d381d687652f2597fe2.js} +4 -4
  35. package/core/built/admin/assets/{chunk.351.cbc224ca65c14ef5322d.js → chunk.351.73f27952f867334a8228.js} +3 -3
  36. package/core/built/admin/assets/{chunk.351.cbc224ca65c14ef5322d.js.LICENSE.txt → chunk.351.73f27952f867334a8228.js.LICENSE.txt} +0 -0
  37. package/core/built/admin/assets/{ghost-b469423d0fbe5e40af17b560f7e3cead.css → ghost-686c383caa6a3469cefb939ab10e21b6.css} +1 -1
  38. package/core/built/admin/assets/{ghost-dark-bcb6f4517a2dfe23a0a280632bfca00c.css → ghost-dark-6814c399ff5b3d9c8efe2d92bc7ec779.css} +1 -1
  39. package/core/built/admin/assets/{ghost-a66a04418efe85083a3adca0fb16bb52.js → ghost-eca1a709a74b1af277e48aad4e16c9db.js} +94 -92
  40. package/core/built/admin/assets/{vendor-46baf13852f545c6c89756c8e0ccbff2.js → vendor-516c9e43b4aeb92079dc1ab92c9ce492.js} +3 -3
  41. package/core/built/admin/index.html +6 -6
  42. package/core/frontend/helpers/comment_count.js +7 -15
  43. package/core/frontend/helpers/comments.js +4 -16
  44. package/core/frontend/helpers/ghost_head.js +1 -1
  45. package/core/frontend/public/ghost.min.css +1 -1
  46. package/core/frontend/src/comment-counts/js/comment-counts.js +12 -1
  47. package/core/server/api/endpoints/comments-members.js +23 -1
  48. package/core/server/api/endpoints/index.js +52 -52
  49. package/core/server/api/endpoints/utils/serializers/input/comments.js +18 -0
  50. package/core/server/api/endpoints/utils/serializers/input/db.js +1 -1
  51. package/core/server/api/endpoints/utils/serializers/input/index.js +4 -0
  52. package/core/server/api/endpoints/utils/serializers/input/integrations.js +2 -2
  53. package/core/server/api/endpoints/utils/serializers/input/tiers.js +1 -1
  54. package/core/server/api/{shared → endpoints/utils}/serializers/input/utils/settings-filter-type-group-mapper.js +0 -0
  55. package/core/server/api/{shared → endpoints/utils}/serializers/input/utils/settings-key-group-mapper.js +0 -0
  56. package/core/server/api/{shared → endpoints/utils}/serializers/input/utils/settings-key-type-mapper.js +0 -0
  57. package/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +12 -12
  58. package/core/server/api/endpoints/utils/serializers/output/tiers.js +1 -1
  59. package/core/server/api/index.js +0 -2
  60. package/core/server/data/db/connection.js +0 -4
  61. package/core/server/data/importer/importers/data/settings.js +2 -2
  62. package/core/server/data/migrations/versions/5.9/2022-08-09-08-32-added-new-integration-type.js +24 -0
  63. package/core/server/data/schema/clients/mysql.js +0 -15
  64. package/core/server/data/schema/commands.js +0 -9
  65. package/core/server/data/schema/fixtures/fixtures.json +1 -1
  66. package/core/server/data/schema/schema.js +3 -3
  67. package/core/server/models/base/plugins/user-type.js +1 -9
  68. package/core/server/models/comment.js +96 -15
  69. package/core/server/models/label.js +14 -0
  70. package/core/server/models/newsletter.js +21 -0
  71. package/core/server/models/stripe-customer-subscription.js +6 -0
  72. package/core/server/models/tag.js +20 -0
  73. package/core/server/models/user.js +20 -0
  74. package/core/server/run-update-check.js +1 -1
  75. package/core/server/services/auth/api-key/admin.js +1 -1
  76. package/core/server/services/auth/api-key/content.js +1 -1
  77. package/core/server/services/bulk-email/bulk-email-processor.js +18 -11
  78. package/core/server/services/bulk-email/index.js +1 -17
  79. package/core/server/services/comments/controller.js +9 -0
  80. package/core/server/services/comments/email-templates/new-comment-reply.hbs +2 -2
  81. package/core/server/services/comments/email-templates/new-comment.hbs +2 -2
  82. package/core/server/services/comments/email-templates/report.hbs +2 -2
  83. package/core/server/services/comments/service.js +16 -3
  84. package/core/server/services/comments/stats.js +2 -0
  85. package/core/server/services/email-analytics/jobs/fetch-latest.js +1 -1
  86. package/core/server/services/mega/post-email-serializer.js +2 -2
  87. package/core/server/services/permissions/can-this.js +154 -161
  88. package/core/server/services/permissions/parse-context.js +1 -8
  89. package/core/server/services/webhooks/serialize.js +3 -3
  90. package/core/server/web/api/endpoints/admin/routes.js +1 -1
  91. package/core/server/web/api/endpoints/content/routes.js +1 -1
  92. package/core/server/web/api/testmode/jobs/graceful-job.js +1 -1
  93. package/core/server/web/comments/routes.js +2 -1
  94. package/core/server/web/shared/middleware/index.js +1 -1
  95. package/core/shared/config/defaults.json +2 -2
  96. package/core/shared/labs.js +0 -1
  97. package/package.json +31 -29
  98. package/yarn.lock +330 -276
  99. package/core/server/api/README.md +0 -130
  100. package/core/server/api/shared/frame.js +0 -95
  101. package/core/server/api/shared/headers.js +0 -152
  102. package/core/server/api/shared/http.js +0 -127
  103. package/core/server/api/shared/index.js +0 -25
  104. package/core/server/api/shared/pipeline.js +0 -259
  105. package/core/server/api/shared/serializers/handle.js +0 -140
  106. package/core/server/api/shared/serializers/index.js +0 -13
  107. package/core/server/api/shared/serializers/input/all.js +0 -41
  108. package/core/server/api/shared/serializers/input/index.js +0 -5
  109. package/core/server/api/shared/serializers/output/index.js +0 -1
  110. package/core/server/api/shared/utils/index.js +0 -5
  111. package/core/server/api/shared/utils/options.js +0 -23
  112. package/core/server/api/shared/validators/handle.js +0 -68
  113. package/core/server/api/shared/validators/index.js +0 -9
  114. package/core/server/api/shared/validators/input/all.js +0 -213
  115. package/core/server/api/shared/validators/input/index.js +0 -5
  116. package/core/server/services/bulk-email/mailgun.js +0 -122
  117. package/core/server/web/shared/middleware/cache-control.js +0 -43
@@ -3,7 +3,7 @@ const debug = require('@tryghost/debug')('api:endpoints:utils:serializers:output
3
3
 
4
4
  const allowedIncludes = ['monthly_price', 'yearly_price'];
5
5
  const localUtils = require('../../index');
6
- const utils = require('../../../../shared/utils');
6
+ const {utils} = require('@tryghost/api-framework');
7
7
  const labs = require('../../../../../../shared/labs');
8
8
 
9
9
  module.exports = {
@@ -1,3 +1 @@
1
1
  module.exports.endpoints = require('./endpoints');
2
-
3
- module.exports.shared = require('./shared');
@@ -31,10 +31,6 @@ function configure(dbConfig) {
31
31
  }
32
32
  };
33
33
 
34
- // Force bthreads to use child_process backend until a worker_thread-compatible version of sqlite3 is published
35
- // https://github.com/mapbox/node-sqlite3/issues/1386
36
- process.env.BTHREADS_BACKEND = 'child_process';
37
-
38
34
  // In the default SQLite test config we set the path to /tmp/ghost-test.db,
39
35
  // but this won't work on Windows, so we need to replace the /tmp bit with
40
36
  // the Windows temp folder
@@ -5,8 +5,8 @@ const _ = require('lodash');
5
5
  const BaseImporter = require('./base');
6
6
  const models = require('../../../../models');
7
7
  const defaultSettings = require('../../../schema').defaultSettings;
8
- const keyGroupMapper = require('../../../../api/shared/serializers/input/utils/settings-key-group-mapper');
9
- const keyTypeMapper = require('../../../../api/shared/serializers/input/utils/settings-key-type-mapper');
8
+ const keyGroupMapper = require('../../../../api/endpoints/utils/serializers/input/utils/settings-key-group-mapper');
9
+ const keyTypeMapper = require('../../../../api/endpoints/utils/serializers/input/utils/settings-key-type-mapper');
10
10
  const {WRITABLE_KEYS_ALLOWLIST} = require('../../../../../shared/labs');
11
11
 
12
12
  const labsDefaults = JSON.parse(defaultSettings.labs.labs.defaultValue);
@@ -0,0 +1,24 @@
1
+ const logging = require('@tryghost/logging');
2
+ const {createTransactionalMigration} = require('../../utils');
3
+
4
+ module.exports = createTransactionalMigration(
5
+ async function up(knex) {
6
+ logging.info('Changing Ghost Explore Integration to type "core"');
7
+ await knex('integrations')
8
+ .where({
9
+ name: 'Ghost Explore',
10
+ slug: 'ghost-explore'
11
+ })
12
+ .update('type', 'core');
13
+ },
14
+ async function down(knex) {
15
+ logging.info('Changing Ghost Explore Integration to type "builtin"');
16
+
17
+ await knex('integrations')
18
+ .where({
19
+ name: 'Ghost Explore',
20
+ slug: 'ghost-explore'
21
+ })
22
+ .update('type', 'builtin');
23
+ }
24
+ );
@@ -27,22 +27,7 @@ const getColumns = function getColumns(table, transaction) {
27
27
  });
28
28
  };
29
29
 
30
- // This function changes the type of posts.html and posts.markdown columns to mediumtext. Due to
31
- // a wrong datatype in schema.js some installations using mysql could have been created using the
32
- // data type text instead of mediumtext.
33
- // For details see: https://github.com/TryGhost/Ghost/issues/1947
34
- const checkPostTable = function checkPostTable(transaction = db.knex) {
35
- return transaction.raw('SHOW FIELDS FROM posts where Field ="html" OR Field = "markdown"').then(function (response) {
36
- return _.flatten(_.map(response[0], function (entry) {
37
- if (entry.Type.toLowerCase() !== 'mediumtext') {
38
- return (transaction || db.knex).raw('ALTER TABLE posts MODIFY ' + entry.Field + ' MEDIUMTEXT');
39
- }
40
- }));
41
- });
42
- };
43
-
44
30
  module.exports = {
45
- checkPostTable: checkPostTable,
46
31
  getTables: getTables,
47
32
  getIndexes: getIndexes,
48
33
  getColumns: getColumns
@@ -411,14 +411,6 @@ function getColumns(table, transaction = db.knex) {
411
411
  return Promise.reject(tpl(messages.noSupportForDatabase, {client: client}));
412
412
  }
413
413
 
414
- function checkTables(transaction = db.knex) {
415
- const client = transaction.client.config.client;
416
-
417
- if (DatabaseInfo.isMySQL(transaction)) {
418
- return clients[client].checkPostTable();
419
- }
420
- }
421
-
422
414
  function createColumnMigration(...migrations) {
423
415
  async function runColumnMigration(conn, migration) {
424
416
  const {
@@ -449,7 +441,6 @@ function createColumnMigration(...migrations) {
449
441
  }
450
442
 
451
443
  module.exports = {
452
- checkTables: checkTables,
453
444
  createTable: createTable,
454
445
  deleteTable: deleteTable,
455
446
  getTables: getTables,
@@ -672,7 +672,7 @@
672
672
  "slug": "ghost-explore",
673
673
  "name": "Ghost Explore",
674
674
  "description": "Built-in Ghost Explore integration",
675
- "type": "builtin",
675
+ "type": "core",
676
676
  "api_keys": [{"type": "admin", "role": "Ghost Explore Integration"}]
677
677
  },
678
678
  {
@@ -319,7 +319,7 @@ module.exports = {
319
319
  maxlength: 50,
320
320
  nullable: false,
321
321
  defaultTo: 'custom',
322
- validations: {isIn: [['internal', 'builtin', 'custom']]}
322
+ validations: {isIn: [['internal', 'builtin', 'custom', 'core']]}
323
323
  },
324
324
  name: {type: 'string', maxlength: 191, nullable: false},
325
325
  slug: {type: 'string', maxlength: 191, nullable: false, unique: true},
@@ -440,9 +440,9 @@ module.exports = {
440
440
  stripe_coupon_id: {type: 'string', maxlength: 255, nullable: true, unique: true},
441
441
  interval: {type: 'string', maxlength: 50, nullable: false, validations: {isIn: [['month', 'year']]}},
442
442
  currency: {type: 'string', maxlength: 50, nullable: true},
443
- discount_type: {type: 'string', maxlength: 50, nullable: false, validations: {isIn: [['percent', 'amount']]}},
443
+ discount_type: {type: 'string', maxlength: 50, nullable: false, validations: {isIn: [['percent', 'amount', 'trial']]}},
444
444
  discount_amount: {type: 'integer', nullable: false},
445
- duration: {type: 'string', maxlength: 50, nullable: false},
445
+ duration: {type: 'string', maxlength: 50, nullable: false, validations: {isIn: [['trial', 'once', 'repeating', 'forever']]}},
446
446
  duration_in_months: {type: 'integer', nullable: true},
447
447
  portal_title: {type: 'string', maxlength: 191, nullable: true},
448
448
  portal_description: {type: 'string', maxlength: 2000, nullable: true},
@@ -33,7 +33,7 @@ module.exports = function (Bookshelf) {
33
33
  options = options || {};
34
34
  options.context = options.context || {};
35
35
 
36
- if (options.context.user || Bookshelf.Model.isExternalUser(options.context.user)) {
36
+ if (options.context.user) {
37
37
  return options.context.user;
38
38
  } else if (options.context.integration) {
39
39
  /**
@@ -64,8 +64,6 @@ module.exports = function (Bookshelf) {
64
64
  return Bookshelf.Model.internalUser;
65
65
  } else if (this.get('id')) {
66
66
  return this.get('id');
67
- } else if (options.context.external) {
68
- return Bookshelf.Model.externalUser;
69
67
  } else {
70
68
  throw new errors.NotFoundError({
71
69
  message: tpl(messages.missingContext),
@@ -80,15 +78,9 @@ module.exports = function (Bookshelf) {
80
78
  * context.user ? true : false (if context.user is 0 as number, this condition is false)
81
79
  */
82
80
  internalUser: 1,
83
- externalUser: 0,
84
81
 
85
82
  isInternalUser: function isInternalUser(id) {
86
83
  return id === Bookshelf.Model.internalUser || id === Bookshelf.Model.internalUser.toString();
87
- },
88
-
89
- isExternalUser: function isExternalUser(id) {
90
- return id === Bookshelf.Model.externalUser || id === Bookshelf.Model.externalUser.toString();
91
84
  }
92
-
93
85
  });
94
86
  };
@@ -8,8 +8,7 @@ const messages = {
8
8
  emptyComment: 'The body of a comment cannot be empty',
9
9
  commentNotFound: 'Comment could not be found',
10
10
  notYourCommentToEdit: 'You may only edit your own comments',
11
- notYourCommentToDestroy: 'You may only delete your own comments',
12
- cannotEditDeletedComment: 'You may only edit published comments'
11
+ notYourCommentToDestroy: 'You may only delete your own comments'
13
12
  };
14
13
 
15
14
  /**
@@ -52,7 +51,10 @@ const Comment = ghostBookshelf.Model.extend({
52
51
  },
53
52
 
54
53
  replies() {
55
- return this.hasMany('Comment', 'parent_id');
54
+ return this.hasMany('Comment', 'parent_id', 'id')
55
+ .query('orderBy', 'created_at', 'ASC')
56
+ // Note: this limit is not working
57
+ .query('limit', 3);
56
58
  },
57
59
 
58
60
  emitChange: function emitChange(event, options) {
@@ -63,12 +65,6 @@ const Comment = ghostBookshelf.Model.extend({
63
65
  onSaving() {
64
66
  ghostBookshelf.Model.prototype.onSaving.apply(this, arguments);
65
67
 
66
- if (this.hasChanged('html') && this.get('status') !== 'published') {
67
- throw new ValidationError({
68
- message: tpl(messages.cannotEditDeletedComment)
69
- });
70
- }
71
-
72
68
  if (this.hasChanged('html')) {
73
69
  const sanitizeHtml = require('sanitize-html');
74
70
 
@@ -105,13 +101,16 @@ const Comment = ghostBookshelf.Model.extend({
105
101
  },
106
102
 
107
103
  enforcedFilters: function enforcedFilters(options) {
108
- if (options.context && options.context.user) {
109
- return null;
104
+ // Convenicence option to merge all filters with parent_id:null filter
105
+ if (options.parentId !== undefined) {
106
+ if (options.parentId === null) {
107
+ return 'parent_id:null';
108
+ }
109
+ return 'parent_id:' + options.parentId;
110
110
  }
111
111
 
112
- return 'parent_id:null';
112
+ return null;
113
113
  }
114
-
115
114
  }, {
116
115
  destroy: function destroy(unfilteredOptions) {
117
116
  let options = this.filterOptions(unfilteredOptions, 'destroy', {extraAllowedProperties: ['id']});
@@ -181,12 +180,94 @@ const Comment = ghostBookshelf.Model.extend({
181
180
  */
182
181
  defaultRelations: function defaultRelations(methodName, options) {
183
182
  // @todo: the default relations are not working for 'add' when we add it below
184
- if (['findAll', 'findPage', 'edit', 'findOne'].indexOf(methodName) !== -1) {
183
+ if (['findAll', 'findPage', 'edit', 'findOne', 'destroy'].indexOf(methodName) !== -1) {
185
184
  if (!options.withRelated || options.withRelated.length === 0) {
186
- options.withRelated = ['member', 'likes', 'replies', 'replies.member', 'replies.likes'];
185
+ if (options.parentId) {
186
+ // Do not include replies for replies
187
+ options.withRelated = [
188
+ // Relations
189
+ 'member', 'count.likes', 'count.liked'
190
+ ];
191
+ } else {
192
+ options.withRelated = [
193
+ // Relations
194
+ 'member', 'count.replies', 'count.likes', 'count.liked',
195
+ // Replies (limited to 3)
196
+ 'replies', 'replies.member' , 'replies.count.likes', 'replies.count.liked'
197
+ ];
198
+ }
187
199
  }
188
200
  }
189
201
 
202
+ return options;
203
+ },
204
+
205
+ async findPage(options) {
206
+ const {withRelated} = this.defaultRelations('findPage', options);
207
+
208
+ const relationsToLoadIndividually = [
209
+ 'replies',
210
+ 'replies.member',
211
+ 'replies.count.likes',
212
+ 'replies.count.liked'
213
+ ].filter(relation => withRelated.includes(relation));
214
+
215
+ const result = await ghostBookshelf.Model.findPage.call(this, options);
216
+
217
+ for (const model of result.data) {
218
+ await model.load(relationsToLoadIndividually, _.omit(options, 'withRelated'));
219
+ }
220
+
221
+ return result;
222
+ },
223
+
224
+ countRelations() {
225
+ return {
226
+ replies(modelOrCollection) {
227
+ modelOrCollection.query('columns', 'comments.*', (qb) => {
228
+ qb.count('replies.id')
229
+ .from('comments AS replies')
230
+ .whereRaw('replies.parent_id = comments.id')
231
+ .as('count__replies');
232
+ });
233
+ },
234
+ likes(modelOrCollection) {
235
+ modelOrCollection.query('columns', 'comments.*', (qb) => {
236
+ qb.count('comment_likes.id')
237
+ .from('comment_likes')
238
+ .whereRaw('comment_likes.comment_id = comments.id')
239
+ .as('count__likes');
240
+ });
241
+ },
242
+ liked(modelOrCollection, options) {
243
+ modelOrCollection.query('columns', 'comments.*', (qb) => {
244
+ if (options.context && options.context.member && options.context.member.id) {
245
+ qb.count('comment_likes.id')
246
+ .from('comment_likes')
247
+ .whereRaw('comment_likes.comment_id = comments.id')
248
+ .where('comment_likes.member_id', options.context.member.id)
249
+ .as('count__liked');
250
+ return;
251
+ }
252
+
253
+ // Return zero
254
+ qb.select(ghostBookshelf.knex.raw('0')).as('count__liked');
255
+ });
256
+ }
257
+ };
258
+ },
259
+
260
+ /**
261
+ * Returns an array of keys permitted in a method's `options` hash, depending on the current method.
262
+ * @param {String} methodName The name of the method to check valid options for.
263
+ * @return {Array} Keys allowed in the `options` hash of the model's method.
264
+ */
265
+ permittedOptions: function permittedOptions(methodName) {
266
+ let options = ghostBookshelf.Model.permittedOptions.call(this, methodName);
267
+
268
+ // The comment model additionally supports having a parentId option
269
+ options.push('parentId');
270
+
190
271
  return options;
191
272
  }
192
273
  });
@@ -107,6 +107,20 @@ Label = ghostBookshelf.Model.extend({
107
107
  return options;
108
108
  },
109
109
 
110
+ countRelations() {
111
+ return {
112
+ members(modelOrCollection) {
113
+ modelOrCollection.query('columns', 'labels.*', (qb) => {
114
+ qb.count('members.id')
115
+ .from('members')
116
+ .leftOuterJoin('members_labels', 'members.id', 'members_labels.member_id')
117
+ .whereRaw('members_labels.label_id = labels.id')
118
+ .as('count__members');
119
+ });
120
+ }
121
+ };
122
+ },
123
+
110
124
  destroy: function destroy(unfilteredOptions) {
111
125
  const options = this.filterOptions(unfilteredOptions, 'destroy', {extraAllowedProperties: ['id']});
112
126
  options.withRelated = ['members'];
@@ -126,6 +126,27 @@ const Newsletter = ghostBookshelf.Model.extend({
126
126
  return options;
127
127
  },
128
128
 
129
+ countRelations() {
130
+ return {
131
+ posts(modelOrCollection) {
132
+ modelOrCollection.query('columns', 'newsletters.*', (qb) => {
133
+ qb.count('posts.id')
134
+ .from('posts')
135
+ .whereRaw('posts.newsletter_id = newsletters.id')
136
+ .as('count__posts');
137
+ });
138
+ },
139
+ members(modelOrCollection) {
140
+ modelOrCollection.query('columns', 'newsletters.*', (qb) => {
141
+ qb.count('members_newsletters.id')
142
+ .from('members_newsletters')
143
+ .whereRaw('members_newsletters.newsletter_id = newsletters.id')
144
+ .as('count__members');
145
+ });
146
+ }
147
+ };
148
+ },
149
+
129
150
  orderDefaultRaw: function () {
130
151
  return 'sort_order ASC, created_at ASC, id ASC';
131
152
  },
@@ -1,5 +1,6 @@
1
1
  const ghostBookshelf = require('./base');
2
2
  const _ = require('lodash');
3
+ const labs = require('../../shared/labs');
3
4
 
4
5
  const StripeCustomerSubscription = ghostBookshelf.Model.extend({
5
6
  tableName: 'members_stripe_customers_subscriptions',
@@ -42,6 +43,11 @@ const StripeCustomerSubscription = ghostBookshelf.Model.extend({
42
43
  current_period_end: defaultSerializedObject.current_period_end
43
44
  };
44
45
 
46
+ if (labs.isSet('freeTrial')) {
47
+ serialized.trial_start_at = defaultSerializedObject.trial_start_at;
48
+ serialized.trial_end_at = defaultSerializedObject.trial_end_at;
49
+ }
50
+
45
51
  if (!_.isEmpty(defaultSerializedObject.stripePrice)) {
46
52
  serialized.price = {
47
53
  id: defaultSerializedObject.stripePrice.stripe_price_id,
@@ -182,6 +182,26 @@ Tag = ghostBookshelf.Model.extend({
182
182
  return options;
183
183
  },
184
184
 
185
+ countRelations() {
186
+ return {
187
+ posts(modelOrCollection, options) {
188
+ modelOrCollection.query('columns', 'tags.*', (qb) => {
189
+ qb.count('posts.id')
190
+ .from('posts')
191
+ .leftOuterJoin('posts_tags', 'posts.id', 'posts_tags.post_id')
192
+ .whereRaw('posts_tags.tag_id = tags.id')
193
+ .as('count__posts');
194
+
195
+ if (options.context && options.context.public) {
196
+ // @TODO use the filter behavior for posts
197
+ qb.andWhere('posts.type', '=', 'post');
198
+ qb.andWhere('posts.status', '=', 'published');
199
+ }
200
+ });
201
+ }
202
+ };
203
+ },
204
+
185
205
  destroy: function destroy(unfilteredOptions) {
186
206
  const options = this.filterOptions(unfilteredOptions, 'destroy', {extraAllowedProperties: ['id']});
187
207
  options.withRelated = ['posts'];
@@ -426,6 +426,26 @@ User = ghostBookshelf.Model.extend({
426
426
  return permittedOptionsToReturn;
427
427
  },
428
428
 
429
+ countRelations() {
430
+ return {
431
+ posts(modelOrCollection, options) {
432
+ modelOrCollection.query('columns', 'users.*', (qb) => {
433
+ qb.count('posts.id')
434
+ .from('posts')
435
+ .join('posts_authors', 'posts.id', 'posts_authors.post_id')
436
+ .whereRaw('posts_authors.author_id = users.id')
437
+ .as('count__posts');
438
+
439
+ if (options.context && options.context.public) {
440
+ // @TODO use the filter behavior for posts
441
+ qb.andWhere('posts.type', '=', 'post');
442
+ qb.andWhere('posts.status', '=', 'published');
443
+ }
444
+ });
445
+ }
446
+ };
447
+ },
448
+
429
449
  /**
430
450
  * ### Find One
431
451
  *
@@ -1,4 +1,4 @@
1
- const {parentPort} = require('bthreads');
1
+ const {parentPort} = require('worker_threads');
2
2
 
3
3
  const postParentPortMessage = (message) => {
4
4
  if (parentPort) {
@@ -127,7 +127,7 @@ const authenticateWithToken = async (req, res, next, {token, JWT_OPTIONS}) => {
127
127
 
128
128
  // CASE: blocking all non-internal: "custom" and "builtin" integration requests when the limit is reached
129
129
  if (limitService.isLimited('customIntegrations')
130
- && (apiKey.relations.integration && !['internal'].includes(apiKey.relations.integration.get('type')))) {
130
+ && (apiKey.relations.integration && !['internal', 'core'].includes(apiKey.relations.integration.get('type')))) {
131
131
  // NOTE: using "checkWouldGoOverLimit" instead of "checkIsOverLimit" here because flag limits don't have
132
132
  // a concept of measuring if the limit has been surpassed
133
133
  await limitService.errorIfWouldGoOverLimit('customIntegrations');
@@ -43,7 +43,7 @@ const authenticateContentApiKey = async function authenticateContentApiKey(req,
43
43
 
44
44
  // CASE: blocking all non-internal: "custom" and "builtin" integration requests when the limit is reached
45
45
  if (limitService.isLimited('customIntegrations')
46
- && (apiKey.relations.integration && !['internal'].includes(apiKey.relations.integration.get('type')))) {
46
+ && (apiKey.relations.integration && !['internal', 'core'].includes(apiKey.relations.integration.get('type')))) {
47
47
  // NOTE: using "checkWouldGoOverLimit" instead of "checkIsOverLimit" here because flag limits don't have
48
48
  // a concept of measuring if the limit has been surpassed
49
49
  await limitService.errorIfWouldGoOverLimit('customIntegrations');
@@ -5,17 +5,19 @@ const errors = require('@tryghost/errors');
5
5
  const tpl = require('@tryghost/tpl');
6
6
  const logging = require('@tryghost/logging');
7
7
  const models = require('../../models');
8
- const mailgunProvider = require('./mailgun');
8
+ const MailgunClient = require('@tryghost/mailgun-client');
9
9
  const sentry = require('../../../shared/sentry');
10
10
  const labs = require('../../../shared/labs');
11
11
  const debug = require('@tryghost/debug')('mega');
12
12
  const postEmailSerializer = require('../mega/post-email-serializer');
13
+ const configService = require('../../../shared/config');
14
+ const settingsCache = require('../../../shared/settings-cache');
13
15
 
14
16
  const messages = {
15
17
  error: 'The email service was unable to send an email batch.'
16
18
  };
17
19
 
18
- const BATCH_SIZE = mailgunProvider.BATCH_SIZE;
20
+ const mailgunClient = new MailgunClient({config: configService, settings: settingsCache});
19
21
 
20
22
  /**
21
23
  * An object representing batch request result
@@ -64,7 +66,7 @@ class FailedBatch extends BatchResultBase {
64
66
  */
65
67
 
66
68
  module.exports = {
67
- BATCH_SIZE,
69
+ BATCH_SIZE: MailgunClient.BATCH_SIZE,
68
70
  SuccessfulBatch,
69
71
  FailedBatch,
70
72
 
@@ -211,11 +213,12 @@ module.exports = {
211
213
  * @param {Email-like} emailData - The email to send, must be a POJO so emailModel.toJSON() before calling if needed
212
214
  * @param {[EmailRecipient]} recipients - The recipients to send the email to with their associated data
213
215
  * @param {string?} memberSegment - The member segment of the recipients
214
- * @returns {Object} - {providerId: 'xxx'}
216
+ * @returns {Promise<Object>} - {providerId: 'xxx'}
215
217
  */
216
- send(emailData, recipients, memberSegment) {
217
- const mailgunInstance = mailgunProvider.getInstance();
218
- if (!mailgunInstance) {
218
+ async send(emailData, recipients, memberSegment) {
219
+ const mailgunConfigured = mailgunClient.isConfigured();
220
+ if (!mailgunConfigured) {
221
+ logging.warn('Bulk email has not been configured');
219
222
  return;
220
223
  }
221
224
 
@@ -247,10 +250,11 @@ module.exports = {
247
250
 
248
251
  emailData = postEmailSerializer.renderEmailForSegment(emailData, memberSegment);
249
252
 
250
- return mailgunProvider.send(emailData, recipientData, replacements).then((response) => {
253
+ try {
254
+ const response = await mailgunClient.send(emailData, recipientData, replacements);
251
255
  debug(`sent message (${Date.now() - startTime}ms)`);
252
256
  return response;
253
- }).catch((error) => {
257
+ } catch (error) {
254
258
  // REF: possible mailgun errors https://documentation.mailgun.com/en/latest/api-intro.html#errors
255
259
  let ghostError = new errors.EmailError({
256
260
  err: error,
@@ -263,6 +267,9 @@ module.exports = {
263
267
 
264
268
  debug(`failed to send message (${Date.now() - startTime}ms)`);
265
269
  throw ghostError;
266
- });
267
- }
270
+ }
271
+ },
272
+
273
+ // NOTE: for testing only!
274
+ _mailgunClient: mailgunClient
268
275
  };
@@ -1,17 +1 @@
1
- const {
2
- BATCH_SIZE,
3
- SuccessfulBatch,
4
- FailedBatch,
5
- processEmail,
6
- processEmailBatch,
7
- send
8
- } = require('./bulk-email-processor');
9
-
10
- module.exports = {
11
- BATCH_SIZE,
12
- SuccessfulBatch,
13
- FailedBatch,
14
- processEmail,
15
- processEmailBatch,
16
- send
17
- };
1
+ module.exports = require('./bulk-email-processor');
@@ -1,3 +1,5 @@
1
+ const _ = require('lodash');
2
+
1
3
  /**
2
4
  * @typedef {import('../../api/shared/frame')} Frame
3
5
  */
@@ -26,6 +28,13 @@ module.exports = class CommentsController {
26
28
  return this.service.getComments(frame.options);
27
29
  }
28
30
 
31
+ /**
32
+ * @param {Frame} frame
33
+ */
34
+ async replies(frame) {
35
+ return this.service.getReplies(frame.options.id, _.omit(frame.options, 'id'));
36
+ }
37
+
29
38
  /**
30
39
  * @param {Frame} frame
31
40
  */
@@ -94,7 +94,7 @@
94
94
  border-bottom-color: #EEF5F8;
95
95
  }
96
96
  a {
97
- color: #3A464C;
97
+ color: #15212A;
98
98
  }
99
99
  </style>
100
100
  </head>
@@ -116,7 +116,7 @@
116
116
  <tr>
117
117
  <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">
118
118
  <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 20px; color: #15212A; font-weight: bold; line-height: 25px; margin: 0; margin-bottom: 15px;">Hey there,</p>
119
- <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 32px;">Someone just replied to your comment on <a href="{{postUrl}}" target="_blank" style="font-weight: bold; text-decoration: underline;">{{postTitle}}</a>.</p>
119
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 32px;">Someone just replied to your comment on <a href="{{postUrl}}" target="_blank" style="font-weight: bold; text-decoration: underline; color: #15212A;">{{postTitle}}</a>.</p>
120
120
 
121
121
  <table width="100" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; table-layout: fixed; width: 100%; min-width: 100%; box-sizing: border-box; background: #F9F9FA; border-radius: 7px;">
122
122
  <tbody>
@@ -94,7 +94,7 @@
94
94
  border-bottom-color: #EEF5F8;
95
95
  }
96
96
  a {
97
- color: #3A464C;
97
+ color: #15212A;
98
98
  }
99
99
  </style>
100
100
  </head>
@@ -116,7 +116,7 @@
116
116
  <tr>
117
117
  <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">
118
118
  <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 20px; color: #15212A; font-weight: bold; line-height: 25px; margin: 0; margin-bottom: 15px;">Hey there,</p>
119
- <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 32px;">Someone just left a comment on your post <a href="{{postUrl}}" target="_blank" style="font-weight: bold; text-decoration: underline;">{{postTitle}}</a>.</p>
119
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 32px;">Someone just left a comment on your post <a href="{{postUrl}}" target="_blank" style="font-weight: bold; text-decoration: underline; color: #15212A;">{{postTitle}}</a>.</p>
120
120
 
121
121
  <table width="100" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; table-layout: fixed; width: 100%; min-width: 100%; box-sizing: border-box; background: #F9F9FA; border-radius: 7px;">
122
122
  <tbody>