ghost 5.18.0 → 5.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.md +1 -1
  2. package/components/tryghost-adapter-manager-5.19.1.tgz +0 -0
  3. package/components/tryghost-api-framework-5.19.1.tgz +0 -0
  4. package/components/tryghost-api-version-compatibility-service-5.19.1.tgz +0 -0
  5. package/components/tryghost-audience-feedback-5.19.1.tgz +0 -0
  6. package/components/tryghost-bootstrap-socket-5.19.1.tgz +0 -0
  7. package/components/tryghost-constants-5.19.1.tgz +0 -0
  8. package/components/tryghost-custom-theme-settings-service-5.19.1.tgz +0 -0
  9. package/components/tryghost-domain-events-5.19.1.tgz +0 -0
  10. package/components/tryghost-email-analytics-provider-mailgun-5.19.1.tgz +0 -0
  11. package/components/tryghost-email-analytics-service-5.19.1.tgz +0 -0
  12. package/components/tryghost-email-content-generator-5.19.1.tgz +0 -0
  13. package/components/tryghost-express-dynamic-redirects-5.19.1.tgz +0 -0
  14. package/components/tryghost-extract-api-key-5.19.1.tgz +0 -0
  15. package/components/tryghost-html-to-plaintext-5.19.1.tgz +0 -0
  16. package/components/{tryghost-job-manager-5.18.0.tgz → tryghost-job-manager-5.19.1.tgz} +0 -0
  17. package/components/tryghost-link-redirects-5.19.1.tgz +0 -0
  18. package/components/tryghost-link-replacer-5.19.1.tgz +0 -0
  19. package/components/tryghost-link-tracking-5.19.1.tgz +0 -0
  20. package/components/{tryghost-magic-link-5.18.0.tgz → tryghost-magic-link-5.19.1.tgz} +0 -0
  21. package/components/tryghost-mailgun-client-5.19.1.tgz +0 -0
  22. package/components/tryghost-member-analytics-service-5.19.1.tgz +0 -0
  23. package/components/tryghost-member-attribution-5.19.1.tgz +0 -0
  24. package/components/tryghost-member-events-5.19.1.tgz +0 -0
  25. package/components/tryghost-members-analytics-ingress-5.19.1.tgz +0 -0
  26. package/components/tryghost-members-api-5.19.1.tgz +0 -0
  27. package/components/tryghost-members-csv-5.19.1.tgz +0 -0
  28. package/components/tryghost-members-events-service-5.19.1.tgz +0 -0
  29. package/components/tryghost-members-importer-5.19.1.tgz +0 -0
  30. package/components/tryghost-members-offers-5.19.1.tgz +0 -0
  31. package/components/tryghost-members-payments-5.19.1.tgz +0 -0
  32. package/components/tryghost-members-ssr-5.19.1.tgz +0 -0
  33. package/components/tryghost-members-stripe-service-5.19.1.tgz +0 -0
  34. package/components/tryghost-minifier-5.19.1.tgz +0 -0
  35. package/components/tryghost-mw-api-version-mismatch-5.19.1.tgz +0 -0
  36. package/components/tryghost-mw-cache-control-5.19.1.tgz +0 -0
  37. package/components/{tryghost-mw-error-handler-5.18.0.tgz → tryghost-mw-error-handler-5.19.1.tgz} +0 -0
  38. package/components/tryghost-mw-session-from-token-5.19.1.tgz +0 -0
  39. package/components/tryghost-mw-update-user-last-seen-5.19.1.tgz +0 -0
  40. package/components/tryghost-mw-vhost-5.19.1.tgz +0 -0
  41. package/components/tryghost-oembed-service-5.19.1.tgz +0 -0
  42. package/components/tryghost-package-json-5.19.1.tgz +0 -0
  43. package/components/{tryghost-referrers-5.18.0.tgz → tryghost-referrers-5.19.1.tgz} +0 -0
  44. package/components/tryghost-security-5.19.1.tgz +0 -0
  45. package/components/tryghost-session-service-5.19.1.tgz +0 -0
  46. package/components/tryghost-settings-path-manager-5.19.1.tgz +0 -0
  47. package/components/tryghost-staff-service-5.19.1.tgz +0 -0
  48. package/components/tryghost-stats-service-5.19.1.tgz +0 -0
  49. package/components/tryghost-update-check-service-5.19.1.tgz +0 -0
  50. package/components/tryghost-verification-trigger-5.19.1.tgz +0 -0
  51. package/components/tryghost-version-notifications-data-service-5.19.1.tgz +0 -0
  52. package/core/boot.js +3 -1
  53. package/core/built/admin/assets/{chunk.143.6d23a3157dae7a9c899d.js → chunk.143.af6972210ca7d2bfdcff.js} +6 -6
  54. package/core/built/admin/assets/{chunk.174.e997dfffceeaa0ce4636.js → chunk.174.3a133d51d9b45097c101.js} +31 -31
  55. package/core/built/admin/assets/{chunk.178.52a9ca26217a593eda67.js → chunk.178.6f68a314b66338b41c62.js} +4 -4
  56. package/core/built/admin/assets/{chunk.427.4483d5bbdaf2a65888ac.js → chunk.613.f1d519ad47e7f9024263.js} +40 -47
  57. package/core/built/admin/assets/{chunk.427.4483d5bbdaf2a65888ac.js.LICENSE.txt → chunk.613.f1d519ad47e7f9024263.js.LICENSE.txt} +0 -0
  58. package/core/built/admin/assets/{ghost-7e6e9479705e7e772bb5ea3b6476cd52.js → ghost-5ce6f5a730c83c91fc258b12c537ea35.js} +555 -559
  59. package/core/built/admin/assets/{ghost-ff0bee94743aa886ce35305a5b46fac3.css → ghost-982146a4ada3a5af1981d1919ae01d08.css} +1 -1
  60. package/core/built/admin/assets/{ghost-dark-a41f7645a406e0df78b7152f3f805e66.css → ghost-dark-41929e4857de411a23597a9de49a4e4f.css} +1 -1
  61. package/core/built/admin/assets/{vendor-4da5d2584fbe1442e25e4271a5513f1c.js → vendor-5c7d7063620bec13668c4370145cd4b4.js} +41 -34
  62. package/core/built/admin/index.html +6 -6
  63. package/core/frontend/helpers/t.js +12 -0
  64. package/core/frontend/helpers/tpl/content-cta.hbs +1 -1
  65. package/core/frontend/public/robots.txt +1 -0
  66. package/core/frontend/services/sitemap/handler.js +1 -1
  67. package/core/frontend/services/sitemap/index-generator.js +1 -3
  68. package/core/frontend/src/comment-counts/js/comment-counts.js +8 -0
  69. package/core/server/api/endpoints/feedback-members.js +23 -0
  70. package/core/server/api/endpoints/index.js +5 -1
  71. package/core/server/api/endpoints/utils/serializers/input/posts.js +6 -1
  72. package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +8 -0
  73. package/core/server/data/exporter/table-lists.js +3 -1
  74. package/core/server/data/importer/handlers/json.js +21 -23
  75. package/core/server/data/importer/importers/data/base.js +1 -1
  76. package/core/server/data/migrations/versions/4.0/05-add-members-subscribe-events-table.js +1 -1
  77. package/core/server/data/migrations/versions/4.0/06-populate-members-subscribe-events-table.js +1 -1
  78. package/core/server/data/migrations/versions/4.0/11-add-members-paid-subscription-events-table.js +1 -1
  79. package/core/server/data/migrations/versions/4.0/13-add-members-payment-events-table.js +1 -1
  80. package/core/server/data/migrations/versions/4.0/17-populate-members-status-events-table.js +1 -1
  81. package/core/server/data/migrations/versions/4.0/22-solve-orphaned-webhooks.js +1 -1
  82. package/core/server/data/migrations/versions/4.0/25-populate-members-paid-subscription-events-table.js +1 -1
  83. package/core/server/data/migrations/versions/4.11/02-add-email-verification-required-setting.js +1 -1
  84. package/core/server/data/migrations/versions/4.12/01-add-email-only-column-to-posts-meta-table.js +1 -1
  85. package/core/server/data/migrations/versions/4.3/03-add-default-product.js +1 -1
  86. package/core/server/data/migrations/versions/4.3/04-attach-members-to-product.js +1 -1
  87. package/core/server/data/migrations/versions/4.3/06-add-stripe-prices-table.js +2 -2
  88. package/core/server/data/migrations/versions/4.3/08-migrate-members-signup-setting.js +1 -1
  89. package/core/server/data/migrations/versions/4.33/2022-01-14-11-51-add-default-free-tier.js +1 -1
  90. package/core/server/data/migrations/versions/4.42/2022-03-21-17-17-add.js +2 -2
  91. package/core/server/data/migrations/versions/4.43/2022-03-28-19-26-recreate-newsletter-table.js +5 -5
  92. package/core/server/data/migrations/versions/4.46/2022-04-13-13-00-add-default-newsletter.js +1 -1
  93. package/core/server/data/migrations/versions/4.46/2022-04-20-08-39-map-subscribers-to-default-newsletter.js +1 -1
  94. package/core/server/data/migrations/versions/4.7/03-add-labs-setting.js +1 -1
  95. package/core/server/data/migrations/versions/4.8/03-add-default-product-portal-products.js +1 -1
  96. package/core/server/data/migrations/versions/4.8/04-migrate-show-newsletter-header-setting.js +1 -1
  97. package/core/server/data/migrations/versions/5.0/2022-05-06-13-22-add-frontend-integration.js +1 -1
  98. package/core/server/data/migrations/versions/5.17/2022-09-29-12-39-add-track-clicks-column-to-emails.js +2 -2
  99. package/core/server/data/migrations/versions/5.19/2022-09-02-20-25-add-columns-to-products-table.js +19 -0
  100. package/core/server/data/migrations/versions/5.19/2022-09-02-20-52-backfill-new-product-columns.js +37 -0
  101. package/core/server/data/migrations/versions/5.19/2022-10-10-06-58-add-subscriptions-table.js +19 -0
  102. package/core/server/data/migrations/versions/5.19/2022-10-10-10-05-add-members-feedback-table.js +10 -0
  103. package/core/server/data/migrations/versions/5.19/2022-10-11-10-38-add-feedback-enabled-column-to-newsletters.js +7 -0
  104. package/core/server/data/schema/commands.js +3 -3
  105. package/core/server/data/schema/fixtures/fixtures.json +4 -1
  106. package/core/server/data/schema/schema.js +90 -24
  107. package/core/server/data/schema/validator.js +1 -1
  108. package/core/server/models/base/bookshelf.js +3 -4
  109. package/core/server/models/base/plugins/data-manipulation.js +1 -1
  110. package/core/server/models/base/plugins/events.js +1 -1
  111. package/core/server/models/base/utils.js +1 -1
  112. package/core/server/models/member-feedback.js +22 -0
  113. package/core/server/models/newsletter.js +3 -2
  114. package/core/server/models/post.js +24 -0
  115. package/core/server/models/settings.js +1 -1
  116. package/core/server/models/user.js +1 -1
  117. package/core/server/services/audience-feedback/FeedbackRepository.js +67 -0
  118. package/core/server/services/audience-feedback/index.js +33 -0
  119. package/core/server/services/bulk-email/bulk-email-processor.js +7 -1
  120. package/core/server/services/mail/GhostMailer.js +17 -1
  121. package/core/server/services/mega/feedback-buttons.js +69 -0
  122. package/core/server/services/mega/mega.js +1 -1
  123. package/core/server/services/mega/post-email-serializer.js +24 -4
  124. package/core/server/services/mega/template.js +3 -0
  125. package/core/server/services/members/middleware.js +40 -0
  126. package/core/server/services/notifications/notifications.js +1 -1
  127. package/core/server/services/settings/settings-service.js +1 -1
  128. package/core/server/services/themes/storage.js +1 -1
  129. package/core/server/web/members/app.js +12 -0
  130. package/core/shared/labs.js +4 -2
  131. package/package.json +111 -111
  132. package/yarn.lock +585 -498
  133. package/components/tryghost-adapter-manager-5.18.0.tgz +0 -0
  134. package/components/tryghost-api-framework-5.18.0.tgz +0 -0
  135. package/components/tryghost-api-version-compatibility-service-5.18.0.tgz +0 -0
  136. package/components/tryghost-bootstrap-socket-5.18.0.tgz +0 -0
  137. package/components/tryghost-constants-5.18.0.tgz +0 -0
  138. package/components/tryghost-custom-theme-settings-service-5.18.0.tgz +0 -0
  139. package/components/tryghost-domain-events-5.18.0.tgz +0 -0
  140. package/components/tryghost-email-analytics-provider-mailgun-5.18.0.tgz +0 -0
  141. package/components/tryghost-email-analytics-service-5.18.0.tgz +0 -0
  142. package/components/tryghost-email-content-generator-5.18.0.tgz +0 -0
  143. package/components/tryghost-express-dynamic-redirects-5.18.0.tgz +0 -0
  144. package/components/tryghost-extract-api-key-5.18.0.tgz +0 -0
  145. package/components/tryghost-html-to-plaintext-5.18.0.tgz +0 -0
  146. package/components/tryghost-link-redirects-5.18.0.tgz +0 -0
  147. package/components/tryghost-link-replacer-5.18.0.tgz +0 -0
  148. package/components/tryghost-link-tracking-5.18.0.tgz +0 -0
  149. package/components/tryghost-mailgun-client-5.18.0.tgz +0 -0
  150. package/components/tryghost-member-analytics-service-5.18.0.tgz +0 -0
  151. package/components/tryghost-member-attribution-5.18.0.tgz +0 -0
  152. package/components/tryghost-member-events-5.18.0.tgz +0 -0
  153. package/components/tryghost-members-analytics-ingress-5.18.0.tgz +0 -0
  154. package/components/tryghost-members-api-5.18.0.tgz +0 -0
  155. package/components/tryghost-members-csv-5.18.0.tgz +0 -0
  156. package/components/tryghost-members-events-service-5.18.0.tgz +0 -0
  157. package/components/tryghost-members-importer-5.18.0.tgz +0 -0
  158. package/components/tryghost-members-offers-5.18.0.tgz +0 -0
  159. package/components/tryghost-members-payments-5.18.0.tgz +0 -0
  160. package/components/tryghost-members-ssr-5.18.0.tgz +0 -0
  161. package/components/tryghost-members-stripe-service-5.18.0.tgz +0 -0
  162. package/components/tryghost-minifier-5.18.0.tgz +0 -0
  163. package/components/tryghost-mw-api-version-mismatch-5.18.0.tgz +0 -0
  164. package/components/tryghost-mw-cache-control-5.18.0.tgz +0 -0
  165. package/components/tryghost-mw-session-from-token-5.18.0.tgz +0 -0
  166. package/components/tryghost-mw-update-user-last-seen-5.18.0.tgz +0 -0
  167. package/components/tryghost-mw-vhost-5.18.0.tgz +0 -0
  168. package/components/tryghost-oembed-service-5.18.0.tgz +0 -0
  169. package/components/tryghost-package-json-5.18.0.tgz +0 -0
  170. package/components/tryghost-security-5.18.0.tgz +0 -0
  171. package/components/tryghost-session-service-5.18.0.tgz +0 -0
  172. package/components/tryghost-settings-path-manager-5.18.0.tgz +0 -0
  173. package/components/tryghost-staff-service-5.18.0.tgz +0 -0
  174. package/components/tryghost-stats-service-5.18.0.tgz +0 -0
  175. package/components/tryghost-update-check-service-5.18.0.tgz +0 -0
  176. package/components/tryghost-verification-trigger-5.18.0.tgz +0 -0
  177. package/components/tryghost-version-notifications-data-service-5.18.0.tgz +0 -0
@@ -13,6 +13,7 @@ module.exports = {
13
13
  uuid: {type: 'string', maxlength: 36, nullable: false, unique: true, validations: {isUUID: true}},
14
14
  name: {type: 'string', maxlength: 191, nullable: false, unique: true},
15
15
  description: {type: 'string', maxlength: 2000, nullable: true},
16
+ feedback_enabled: {type: 'boolean', nullable: false, defaultTo: false},
16
17
  slug: {type: 'string', maxlength: 191, nullable: false, unique: true},
17
18
  sender_name: {type: 'string', maxlength: 191, nullable: true},
18
19
  sender_email: {type: 'string', maxlength: 191, nullable: true},
@@ -24,18 +25,18 @@ module.exports = {
24
25
  nullable: false,
25
26
  defaultTo: 'members'
26
27
  },
27
- subscribe_on_signup: {type: 'bool', nullable: false, defaultTo: true},
28
+ subscribe_on_signup: {type: 'boolean', nullable: false, defaultTo: true},
28
29
  sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0},
29
30
  header_image: {type: 'string', maxlength: 2000, nullable: true},
30
- show_header_icon: {type: 'bool', nullable: false, defaultTo: true},
31
- show_header_title: {type: 'bool', nullable: false, defaultTo: true},
31
+ show_header_icon: {type: 'boolean', nullable: false, defaultTo: true},
32
+ show_header_title: {type: 'boolean', nullable: false, defaultTo: true},
32
33
  title_font_category: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'sans_serif', validations: {isIn: [['serif', 'sans_serif']]}},
33
34
  title_alignment: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'center', validations: {isIn: [['center', 'left']]}},
34
- show_feature_image: {type: 'bool', nullable: false, defaultTo: true},
35
+ show_feature_image: {type: 'boolean', nullable: false, defaultTo: true},
35
36
  body_font_category: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'sans_serif', validations: {isIn: [['serif', 'sans_serif']]}},
36
37
  footer_content: {type: 'text', maxlength: 1000000000, nullable: true},
37
- show_badge: {type: 'bool', nullable: false, defaultTo: true},
38
- show_header_name: {type: 'bool', nullable: false, defaultTo: true},
38
+ show_badge: {type: 'boolean', nullable: false, defaultTo: true},
39
+ show_header_name: {type: 'boolean', nullable: false, defaultTo: true},
39
40
  created_at: {type: 'dateTime', nullable: false},
40
41
  updated_at: {type: 'dateTime', nullable: true}
41
42
  },
@@ -50,7 +51,7 @@ module.exports = {
50
51
  comment_id: {type: 'string', maxlength: 50, nullable: true},
51
52
  plaintext: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
52
53
  feature_image: {type: 'string', maxlength: 2000, nullable: true},
53
- featured: {type: 'bool', nullable: false, defaultTo: false},
54
+ featured: {type: 'boolean', nullable: false, defaultTo: false},
54
55
  type: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'post', validations: {isIn: [['post', 'page']]}},
55
56
  status: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'draft', validations: {isIn: [['published', 'draft', 'scheduled', 'sent']]}},
56
57
  // NOTE: unused at the moment and reserved for future features
@@ -102,7 +103,7 @@ module.exports = {
102
103
  frontmatter: {type: 'text', maxlength: 65535, nullable: true},
103
104
  feature_image_alt: {type: 'string', maxlength: 191, nullable: true, validations: {isLength: {max: 125}}},
104
105
  feature_image_caption: {type: 'text', maxlength: 65535, nullable: true},
105
- email_only: {type: 'bool', nullable: false, defaultTo: false}
106
+ email_only: {type: 'boolean', nullable: false, defaultTo: false}
106
107
  },
107
108
  // NOTE: this is the staff table
108
109
  users: {
@@ -423,7 +424,7 @@ module.exports = {
423
424
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
424
425
  name: {type: 'string', maxlength: 191, nullable: false},
425
426
  slug: {type: 'string', maxlength: 191, nullable: false, unique: true},
426
- // @deprecated: use a status enum with isIn validation, not aan ctive boolean
427
+ // @deprecated: use a status enum with isIn validation, not an `active` boolean
427
428
  active: {type: 'boolean', nullable: false, defaultTo: true},
428
429
  welcome_page_url: {type: 'string', maxlength: 2000, nullable: true},
429
430
  visibility: {
@@ -434,16 +435,28 @@ module.exports = {
434
435
  validations: {isIn: [['public', 'none']]}
435
436
  },
436
437
  trial_days: {type: 'integer', unsigned: true, nullable: false, defaultTo: 0},
437
- monthly_price_id: {type: 'string', maxlength: 24, nullable: true},
438
- yearly_price_id: {type: 'string', maxlength: 24, nullable: true},
439
438
  description: {type: 'string', maxlength: 191, nullable: true},
440
- type: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'paid', validations: {isIn: [['paid', 'free']]}},
439
+ type: {
440
+ type: 'string',
441
+ maxlength: 50,
442
+ nullable: false,
443
+ defaultTo: 'paid',
444
+ validations: {
445
+ isIn: [['paid', 'free']]
446
+ }
447
+ },
448
+ currency: {type: 'string', maxlength: 50, nullable: true},
449
+ monthly_price: {type: 'integer', unsigned: true, nullable: true},
450
+ yearly_price: {type: 'integer', unsigned: true, nullable: true},
441
451
  created_at: {type: 'dateTime', nullable: false},
442
- updated_at: {type: 'dateTime', nullable: true}
452
+ updated_at: {type: 'dateTime', nullable: true},
453
+ // To be removed in future
454
+ monthly_price_id: {type: 'string', maxlength: 24, nullable: true},
455
+ yearly_price_id: {type: 'string', maxlength: 24, nullable: true}
443
456
  },
444
457
  offers: {
445
458
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
446
- // @deprecated: use a status enum with isIn validation, not aan ctive boolean
459
+ // @deprecated: use a status enum with isIn validation, not an `active` boolean
447
460
  active: {type: 'boolean', nullable: false, defaultTo: true},
448
461
  name: {type: 'string', maxlength: 191, nullable: false, unique: true},
449
462
  code: {type: 'string', maxlength: 191, nullable: false, unique: true},
@@ -516,7 +529,9 @@ module.exports = {
516
529
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
517
530
  member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
518
531
  amount: {type: 'integer', nullable: false},
519
- currency: {type: 'string', maxLength: 3, nullable: false},
532
+ // @note: this is longer than originally intended due to a bug - https://github.com/TryGhost/Ghost/pull/15606
533
+ // so we should decide whether we should reduce it down in the future
534
+ currency: {type: 'string', maxlength: 191, nullable: false},
520
535
  source: {type: 'string', maxlength: 50, nullable: false},
521
536
  created_at: {type: 'dateTime', nullable: false}
522
537
  },
@@ -565,7 +580,9 @@ module.exports = {
565
580
  subscription_id: {type: 'string', maxlength: 24, nullable: true},
566
581
  from_plan: {type: 'string', maxlength: 255, nullable: true},
567
582
  to_plan: {type: 'string', maxlength: 255, nullable: true},
568
- currency: {type: 'string', maxLength: 3, nullable: false},
583
+ // @note: this is longer than originally intended due to a bug - https://github.com/TryGhost/Ghost/pull/15606
584
+ // so we should decide whether we should reduce it down in the future
585
+ currency: {type: 'string', maxlength: 191, nullable: false},
569
586
  source: {
570
587
  type: 'string', maxlength: 50, nullable: false, validations: {
571
588
  isIn: [['stripe']]
@@ -600,13 +617,50 @@ module.exports = {
600
617
  updated_at: {type: 'dateTime', nullable: true},
601
618
  updated_by: {type: 'string', maxlength: 24, nullable: true}
602
619
  },
620
+ subscriptions: {
621
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
622
+ type: {
623
+ type: 'string', maxlength: 50, nullable: false, validations: {
624
+ isIn: [['free', 'comped', 'paid']]
625
+ }
626
+ },
627
+ status: {
628
+ type: 'string', maxlength: 50, nullable: false, validations: {
629
+ isIn: [['active', 'expired', 'canceled']]
630
+ }
631
+ },
632
+ member_id: {type: 'string', maxlength: 24, nullable: false, unique: false, references: 'members.id', cascadeDelete: true},
633
+ tier_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'products.id'},
634
+
635
+ // These are null if type !== 'paid'
636
+ cadence: {
637
+ type: 'string', maxlength: 50, nullable: true, validations: {
638
+ isIn: [['month', 'year']]
639
+ }
640
+ },
641
+ currency: {type: 'string', maxlength: 50, nullable: true},
642
+ amount: {type: 'integer', nullable: true},
643
+
644
+ // e.g. 'stripe'
645
+ payment_provider: {type: 'string', maxlength: 50, nullable: true},
646
+ // e.g. Stripe Subscription Link
647
+ payment_subscription_url: {type: 'string', maxlength: 2000, nullable: true},
648
+ // e.g. Stripe Customer Link
649
+ payment_user_url: {type: 'string', maxlength: 2000, nullable: true},
650
+
651
+ offer_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'offers.id'},
652
+
653
+ expires_at: {type: 'dateTime', nullable: true},
654
+ created_at: {type: 'dateTime', nullable: false},
655
+ updated_at: {type: 'dateTime', nullable: true}
656
+ },
603
657
  members_stripe_customers_subscriptions: {
604
658
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
605
659
  customer_id: {type: 'string', maxlength: 255, nullable: false, unique: false, references: 'members_stripe_customers.customer_id', cascadeDelete: true},
606
660
  subscription_id: {type: 'string', maxlength: 255, nullable: false, unique: true},
607
661
  stripe_price_id: {type: 'string', maxlength: 255, nullable: false, unique: false, index: true, defaultTo: ''},
608
662
  status: {type: 'string', maxlength: 50, nullable: false},
609
- cancel_at_period_end: {type: 'bool', nullable: false, defaultTo: false},
663
+ cancel_at_period_end: {type: 'boolean', nullable: false, defaultTo: false},
610
664
  cancellation_reason: {type: 'string', maxlength: 500, nullable: true},
611
665
  current_period_end: {type: 'dateTime', nullable: false},
612
666
  start_date: {type: 'dateTime', nullable: false},
@@ -619,12 +673,14 @@ module.exports = {
619
673
  offer_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'offers.id'},
620
674
  trial_start_at: {type: 'dateTime', nullable: true},
621
675
  trial_end_at: {type: 'dateTime', nullable: true},
622
- /* Below fields are now redundant as we link prie_id to stripe_prices table */
676
+ /* Below fields are now redundant as we link stripe_price_id to stripe_prices table */
623
677
  plan_id: {type: 'string', maxlength: 255, nullable: false, unique: false},
624
678
  plan_nickname: {type: 'string', maxlength: 50, nullable: false},
625
679
  plan_interval: {type: 'string', maxlength: 50, nullable: false},
626
680
  plan_amount: {type: 'integer', nullable: false},
627
- plan_currency: {type: 'string', maxLength: 3, nullable: false}
681
+ // @note: this is longer than originally intended due to a bug - https://github.com/TryGhost/Ghost/pull/15606
682
+ // so we should decide whether we should reduce it down in the future
683
+ plan_currency: {type: 'string', maxlength: 191, nullable: false}
628
684
  },
629
685
  members_subscription_created_events: {
630
686
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
@@ -652,7 +708,7 @@ module.exports = {
652
708
  members_subscribe_events: {
653
709
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
654
710
  member_id: {type: 'string', maxlength: 24, nullable: false, unique: false, references: 'members.id', cascadeDelete: true},
655
- subscribed: {type: 'bool', nullable: false, defaultTo: true},
711
+ subscribed: {type: 'boolean', nullable: false, defaultTo: true},
656
712
  created_at: {type: 'dateTime', nullable: false},
657
713
  source: {
658
714
  type: 'string', maxlength: 50, nullable: true, validations: {
@@ -672,9 +728,11 @@ module.exports = {
672
728
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
673
729
  stripe_price_id: {type: 'string', maxlength: 255, nullable: false, unique: true},
674
730
  stripe_product_id: {type: 'string', maxlength: 255, nullable: false, unique: false, references: 'stripe_products.stripe_product_id'},
675
- active: {type: 'bool', nullable: false},
731
+ active: {type: 'boolean', nullable: false},
676
732
  nickname: {type: 'string', maxlength: 50, nullable: true},
677
- currency: {type: 'string', maxLength: 3, nullable: false},
733
+ // @note: this is longer than originally intended due to a bug - https://github.com/TryGhost/Ghost/pull/15606
734
+ // so we should decide whether we should reduce it down in the future
735
+ currency: {type: 'string', maxlength: 191, nullable: false},
678
736
  amount: {type: 'integer', nullable: false},
679
737
  type: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'recurring', validations: {isIn: [['recurring', 'one_time']]}},
680
738
  interval: {type: 'string', maxlength: 50, nullable: true},
@@ -722,8 +780,8 @@ module.exports = {
722
780
  reply_to: {type: 'string', maxlength: 2000, nullable: true},
723
781
  html: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
724
782
  plaintext: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
725
- track_opens: {type: 'bool', nullable: false, defaultTo: false},
726
- track_clicks: {type: 'bool', nullable: false, defaultTo: false},
783
+ track_opens: {type: 'boolean', nullable: false, defaultTo: false},
784
+ track_clicks: {type: 'boolean', nullable: false, defaultTo: false},
727
785
  submitted_at: {type: 'dateTime', nullable: false},
728
786
  newsletter_id: {type: 'string', maxlength: 24, nullable: true, references: 'newsletters.id'},
729
787
  created_at: {type: 'dateTime', nullable: false},
@@ -850,5 +908,13 @@ module.exports = {
850
908
  member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
851
909
  redirect_id: {type: 'string', maxlength: 24, nullable: false, references: 'redirects.id', cascadeDelete: true},
852
910
  created_at: {type: 'dateTime', nullable: false}
911
+ },
912
+ members_feedback: {
913
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
914
+ score: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0},
915
+ member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
916
+ post_id: {type: 'string', maxlength: 24, nullable: false, references: 'posts.id', cascadeDelete: true},
917
+ created_at: {type: 'dateTime', nullable: false},
918
+ updated_at: {type: 'dateTime', nullable: true}
853
919
  }
854
920
  };
@@ -63,7 +63,7 @@ function validateSchema(tableName, model, options) {
63
63
 
64
64
  // validate boolean columns
65
65
  if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'type')
66
- && schema[tableName][columnKey].type === 'bool') {
66
+ && schema[tableName][columnKey].type === 'boolean') {
67
67
  if (!(validator.isBoolean(strVal) || validator.isEmpty(strVal))) {
68
68
  message = tpl(messages.valueMustBeBoolean, {
69
69
  tableName: tableName,
@@ -1,8 +1,7 @@
1
1
  const _ = require('lodash');
2
2
  const bookshelf = require('bookshelf');
3
- const ObjectId = require('bson-objectid');
3
+ const ObjectId = require('bson-objectid').default;
4
4
  const plugins = require('@tryghost/bookshelf-plugins');
5
- const Promise = require('bluebird');
6
5
 
7
6
  const db = require('../../data/db');
8
7
 
@@ -84,13 +83,13 @@ ghostBookshelf.plugin('bookshelf-relations', {
84
83
  return Promise.resolve();
85
84
  }
86
85
 
87
- return Promise.each(targets.models, function (target, index) {
86
+ return Promise.all(targets.models.map((target, index) => {
88
87
  queryOptions.query.where[existing.relatedData.otherKey] = target.id;
89
88
 
90
89
  return existing.updatePivot({
91
90
  sort_order: index
92
91
  }, _.extend({}, options, queryOptions));
93
- });
92
+ }));
94
93
  },
95
94
  beforeRelationCreation: function onCreatingRelation(model, data) {
96
95
  data.id = ObjectId().toHexString();
@@ -78,7 +78,7 @@ module.exports = function (Bookshelf) {
78
78
  _.each(attrs, function each(value, key) {
79
79
  const tableDef = schema.tables[self.tableName];
80
80
  const columnDef = tableDef ? tableDef[key] : null;
81
- if (columnDef && (columnDef.type === 'bool' || columnDef.type === 'boolean')) {
81
+ if (columnDef?.type === 'boolean') {
82
82
  attrs[key] = value ? true : false;
83
83
  }
84
84
  });
@@ -1,6 +1,6 @@
1
1
  const _ = require('lodash');
2
2
  const debug = require('@tryghost/debug')('models:base:model-events');
3
- const ObjectId = require('bson-objectid');
3
+ const ObjectId = require('bson-objectid').default;
4
4
 
5
5
  const schema = require('../../../data/schema');
6
6
 
@@ -5,7 +5,7 @@
5
5
  const _ = require('lodash');
6
6
 
7
7
  const Promise = require('bluebird');
8
- const ObjectId = require('bson-objectid');
8
+ const ObjectId = require('bson-objectid').default;
9
9
  const errors = require('@tryghost/errors');
10
10
 
11
11
  /**
@@ -0,0 +1,22 @@
1
+ const errors = require('@tryghost/errors');
2
+ const ghostBookshelf = require('./base');
3
+
4
+ const MemberFeedback = ghostBookshelf.Model.extend({
5
+ tableName: 'members_feedback',
6
+
7
+ post() {
8
+ return this.belongsTo('Post', 'post_id', 'id');
9
+ },
10
+
11
+ member() {
12
+ return this.belongsTo('Member', 'member_id', 'id');
13
+ }
14
+ }, {
15
+ async destroy() {
16
+ throw new errors.IncorrectUsageError({message: 'Cannot destroy MemberFeedback'});
17
+ }
18
+ });
19
+
20
+ module.exports = {
21
+ MemberFeedback: ghostBookshelf.model('MemberFeedback', MemberFeedback)
22
+ };
@@ -1,5 +1,5 @@
1
1
  const ghostBookshelf = require('./base');
2
- const ObjectID = require('bson-objectid');
2
+ const ObjectID = require('bson-objectid').default;
3
3
  const uuid = require('uuid');
4
4
  const urlUtils = require('../../shared/url-utils');
5
5
 
@@ -21,7 +21,8 @@ const Newsletter = ghostBookshelf.Model.extend({
21
21
  show_badge: true,
22
22
  show_header_icon: true,
23
23
  show_header_title: true,
24
- show_header_name: true
24
+ show_header_name: true,
25
+ feedback_enabled: false
25
26
  };
26
27
  },
27
28
 
@@ -1354,6 +1354,30 @@ Post = ghostBookshelf.Model.extend({
1354
1354
  .whereRaw('posts.id = redirects.post_id')
1355
1355
  .as('count__clicks');
1356
1356
  });
1357
+ },
1358
+ sentiment(modelOrCollection) {
1359
+ modelOrCollection.query('columns', 'posts.*', (qb) => {
1360
+ qb.select(qb.client.raw('ROUND(AVG(score) * 100)'))
1361
+ .from('members_feedback')
1362
+ .whereRaw('posts.id = members_feedback.post_id')
1363
+ .as('count__sentiment');
1364
+ });
1365
+ },
1366
+ negative_feedback(modelOrCollection) {
1367
+ modelOrCollection.query('columns', 'posts.*', (qb) => {
1368
+ qb.count('*')
1369
+ .from('members_feedback')
1370
+ .whereRaw('posts.id = members_feedback.post_id AND members_feedback.score = 0')
1371
+ .as('count__positive_feedback');
1372
+ });
1373
+ },
1374
+ positive_feedback(modelOrCollection) {
1375
+ modelOrCollection.query('columns', 'posts.*', (qb) => {
1376
+ qb.sum('score')
1377
+ .from('members_feedback')
1378
+ .whereRaw('posts.id = members_feedback.post_id')
1379
+ .as('count__positive_feedback');
1380
+ });
1357
1381
  }
1358
1382
  };
1359
1383
  }
@@ -3,7 +3,7 @@ const _ = require('lodash');
3
3
  const uuid = require('uuid');
4
4
  const crypto = require('crypto');
5
5
  const keypair = require('keypair');
6
- const ObjectID = require('bson-objectid');
6
+ const ObjectID = require('bson-objectid').default;
7
7
  const ghostBookshelf = require('./base');
8
8
  const tpl = require('@tryghost/tpl');
9
9
  const errors = require('@tryghost/errors');
@@ -1,7 +1,7 @@
1
1
  const _ = require('lodash');
2
2
  const Promise = require('bluebird');
3
3
  const validator = require('@tryghost/validator');
4
- const ObjectId = require('bson-objectid');
4
+ const ObjectId = require('bson-objectid').default;
5
5
  const ghostBookshelf = require('./base');
6
6
  const baseUtils = require('./base/utils');
7
7
  const limitService = require('../services/limits');
@@ -0,0 +1,67 @@
1
+ module.exports = class FeedbackRepository {
2
+ /** @type {object} */
3
+ #Member;
4
+
5
+ /** @type {object} */
6
+ #Post;
7
+
8
+ /** @type {object} */
9
+ #MemberFeedback;
10
+
11
+ /** @type {typeof Object} */
12
+ #Feedback;
13
+
14
+ /**
15
+ * @param {object} deps
16
+ * @param {object} deps.Member Bookshelf Model
17
+ * @param {object} deps.Post Bookshelf Model
18
+ * @param {object} deps.MemberFeedback Bookshelf Model
19
+ * @param {object} deps.Feedback Feedback object
20
+ */
21
+ constructor(deps) {
22
+ this.#Member = deps.Member;
23
+ this.#Post = deps.Post;
24
+ this.#MemberFeedback = deps.MemberFeedback;
25
+ this.#Feedback = deps.Feedback;
26
+ }
27
+
28
+ async add(feedback) {
29
+ await this.#MemberFeedback.add({
30
+ id: feedback.id.toHexString(),
31
+ member_id: feedback.memberId.toHexString(),
32
+ post_id: feedback.postId.toHexString(),
33
+ score: feedback.score
34
+ });
35
+ }
36
+
37
+ async edit(feedback) {
38
+ await this.#MemberFeedback.edit({
39
+ score: feedback.score
40
+ }, {
41
+ id: feedback.id.toHexString()
42
+ });
43
+ }
44
+
45
+ async get(postId, memberId) {
46
+ const model = await this.#MemberFeedback.findOne({member_id: memberId, post_id: postId}, {require: false});
47
+
48
+ if (!model) {
49
+ return;
50
+ }
51
+
52
+ return new this.#Feedback({
53
+ id: model.id,
54
+ memberId: model.get('member_id'),
55
+ postId: model.get('post_id'),
56
+ score: model.get('score')
57
+ });
58
+ }
59
+
60
+ async getMemberByUuid(uuid) {
61
+ return await this.#Member.findOne({uuid});
62
+ }
63
+
64
+ async getPostById(id) {
65
+ return await this.#Post.findOne({id});
66
+ }
67
+ };
@@ -0,0 +1,33 @@
1
+ const urlUtils = require('../../../shared/url-utils');
2
+ const FeedbackRepository = require('./FeedbackRepository');
3
+
4
+ class AudienceFeedbackServiceWrapper {
5
+ async init() {
6
+ if (this.service) {
7
+ // Already done
8
+ return;
9
+ }
10
+
11
+ // Wire up all the dependencies
12
+ const models = require('../../models');
13
+
14
+ const {AudienceFeedbackService, AudienceFeedbackController, Feedback} = require('@tryghost/audience-feedback');
15
+
16
+ this.repository = new FeedbackRepository({
17
+ Member: models.Member,
18
+ MemberFeedback: models.MemberFeedback,
19
+ Feedback,
20
+ Post: models.Post
21
+ });
22
+
23
+ // Expose the service
24
+ this.service = new AudienceFeedbackService({
25
+ config: {
26
+ baseURL: new URL(urlUtils.urlFor('home', true))
27
+ }
28
+ });
29
+ this.controller = new AudienceFeedbackController({repository: this.repository});
30
+ }
31
+ }
32
+
33
+ module.exports = new AudienceFeedbackServiceWrapper();
@@ -11,6 +11,7 @@ const debug = require('@tryghost/debug')('mega');
11
11
  const postEmailSerializer = require('../mega/post-email-serializer');
12
12
  const configService = require('../../../shared/config');
13
13
  const settingsCache = require('../../../shared/settings-cache');
14
+ const labs = require('../../../shared/labs');
14
15
 
15
16
  const messages = {
16
17
  error: 'The email service received an error from mailgun and was unable to send.'
@@ -208,7 +209,7 @@ module.exports = {
208
209
 
209
210
  /**
210
211
  * @param {Email-like} emailData - The email to send, must be a POJO so emailModel.toJSON() before calling if needed
211
- * @param {[EmailRecipient]} recipients - The recipients to send the email to with their associated data
212
+ * @param {EmailRecipient[]} recipients - The recipients to send the email to with their associated data
212
213
  * @param {string?} memberSegment - The member segment of the recipients
213
214
  * @returns {Promise<Object>} - {providerId: 'xxx'}
214
215
  */
@@ -234,6 +235,11 @@ module.exports = {
234
235
  unsubscribe_url: postEmailSerializer.createUnsubscribeUrl(recipient.member_uuid, {newsletterUuid})
235
236
  };
236
237
 
238
+ if (labs.isSet('audienceFeedback')) {
239
+ // create unique urls for every recipient (for example, for feedback buttons)
240
+ emailData = postEmailSerializer.createUserLinks(emailData, recipient.member_uuid);
241
+ }
242
+
237
243
  // computed properties on recipients - TODO: better way of handling these
238
244
  recipient.member_first_name = (recipient.member_name || '').split(' ')[0];
239
245
 
@@ -7,6 +7,7 @@ const errors = require('@tryghost/errors');
7
7
  const tpl = require('@tryghost/tpl');
8
8
  const settingsCache = require('../../../shared/settings-cache');
9
9
  const urlUtils = require('../../../shared/url-utils');
10
+ const metrics = require('@tryghost/metrics');
10
11
  const messages = {
11
12
  title: 'Ghost at {domain}',
12
13
  checkEmailConfigInstructions: 'Please see {url} for instructions on configuring email.',
@@ -83,7 +84,8 @@ module.exports = class GhostMailer {
83
84
  const options = config.get('mail') && _.clone(config.get('mail').options) || {};
84
85
 
85
86
  this.state = {
86
- usingDirect: transport === 'direct'
87
+ usingDirect: transport === 'direct',
88
+ usingMailgun: transport === 'mailgun'
87
89
  };
88
90
  this.transport = nodemailer(transport, options);
89
91
  }
@@ -121,10 +123,24 @@ module.exports = class GhostMailer {
121
123
  }
122
124
 
123
125
  async sendMail(message) {
126
+ const startTime = Date.now();
124
127
  try {
125
128
  const response = await this.transport.sendMail(message);
129
+ if (this.state.usingMailgun) {
130
+ metrics.metric('mailgun-send-transactional-mail', {
131
+ value: Date.now() - startTime,
132
+ statusCode: 200
133
+ });
134
+ }
135
+
126
136
  return response;
127
137
  } catch (err) {
138
+ if (this.state.usingMailgun) {
139
+ metrics.metric('mailgun-send-transactional-mail', {
140
+ value: Date.now() - startTime,
141
+ statusCode: err.status
142
+ });
143
+ }
128
144
  throw createMailError({
129
145
  message: tpl(messages.reason, {reason: err.message || err}),
130
146
  err
@@ -0,0 +1,69 @@
1
+ const {Color} = require('@tryghost/color-utils');
2
+ const audienceFeedback = require('../audience-feedback');
3
+
4
+ const templateStrings = {
5
+ like: '%{feedback_button_like}%',
6
+ dislike: '%{feedback_button_dislike}%'
7
+ };
8
+
9
+ const generateLinks = (postId, uuid, html) => {
10
+ const positiveLink = audienceFeedback.service.buildLink(
11
+ uuid,
12
+ postId,
13
+ 1
14
+ );
15
+ const negativeLink = audienceFeedback.service.buildLink(
16
+ uuid,
17
+ postId,
18
+ 0
19
+ );
20
+
21
+ html = html.replace(templateStrings.like, positiveLink.href);
22
+ html = html.replace(templateStrings.dislike, negativeLink.href);
23
+
24
+ return html;
25
+ };
26
+
27
+ const getTemplate = (accentColor) => {
28
+ const likeButtonHtml = getButtonHtml(templateStrings.like, 'More like this', accentColor);
29
+ const dislikeButtonHtml = getButtonHtml(templateStrings.dislike, 'Less like this', accentColor);
30
+
31
+ return (`
32
+ <tr>
33
+ <td dir="ltr" width="100%" style="background-color: #ffffff; text-align: center; padding: 40px 4px; border-bottom: 1px solid #e5eff5" align="center">
34
+ <h3 style="text-align: center; margin-bottom: 22px; font-size: 17px; letter-spacing: -0.2px; margin-top: 0 !important;">What did you think of this post?</h3>
35
+ <table role="presentation" border="0" cellpadding="0" cellspacing="0" style="margin: auto; width: auto !important;">
36
+ <tr>
37
+ ${likeButtonHtml}
38
+ ${dislikeButtonHtml}
39
+ </tr>
40
+ </table>
41
+ </td>
42
+ </tr>
43
+ `);
44
+ };
45
+
46
+ function getButtonHtml(href, buttonText, accentColor) {
47
+ const color = new Color(accentColor);
48
+ const bgColor = `${accentColor}10`;
49
+ const textColor = color.darken(0.6).hex();
50
+
51
+ return (`
52
+ <td dir="ltr" valign="top" align="center" style="font-family: inherit; font-size: 14px; text-align: center;" nowrap>
53
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="width: auto !important;">
54
+ <tr>
55
+ <td style="padding: 0 6px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';">
56
+ <a href=${href} style="background-color: ${bgColor}; color: ${textColor}; border-radius: 22px; font-family: inherit; padding: 12px 20px; border: none; font-size: 14px; font-weight: bold; line-height: 100%; text-decoration: none; display: block;">
57
+ ${buttonText}
58
+ </a>
59
+ </td>
60
+ </tr>
61
+ </table>
62
+ </td>
63
+ `);
64
+ }
65
+
66
+ module.exports = {
67
+ generateLinks,
68
+ getTemplate
69
+ };
@@ -3,7 +3,7 @@ const Promise = require('bluebird');
3
3
  const debug = require('@tryghost/debug')('mega');
4
4
  const tpl = require('@tryghost/tpl');
5
5
  const moment = require('moment');
6
- const ObjectID = require('bson-objectid');
6
+ const ObjectID = require('bson-objectid').default;
7
7
  const errors = require('@tryghost/errors');
8
8
  const logging = require('@tryghost/logging');
9
9
  const settingsCache = require('../../../shared/settings-cache');