ghost 5.17.2 → 5.19.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 (190) hide show
  1. package/README.md +3 -3
  2. package/components/tryghost-adapter-manager-5.19.0.tgz +0 -0
  3. package/components/{tryghost-api-framework-5.17.2.tgz → tryghost-api-framework-5.19.0.tgz} +0 -0
  4. package/components/tryghost-api-version-compatibility-service-5.19.0.tgz +0 -0
  5. package/components/tryghost-audience-feedback-5.19.0.tgz +0 -0
  6. package/components/tryghost-bootstrap-socket-5.19.0.tgz +0 -0
  7. package/components/tryghost-constants-5.19.0.tgz +0 -0
  8. package/components/{tryghost-custom-theme-settings-service-5.17.2.tgz → tryghost-custom-theme-settings-service-5.19.0.tgz} +0 -0
  9. package/components/tryghost-domain-events-5.19.0.tgz +0 -0
  10. package/components/tryghost-email-analytics-provider-mailgun-5.19.0.tgz +0 -0
  11. package/components/tryghost-email-analytics-service-5.19.0.tgz +0 -0
  12. package/components/tryghost-email-content-generator-5.19.0.tgz +0 -0
  13. package/components/tryghost-express-dynamic-redirects-5.19.0.tgz +0 -0
  14. package/components/tryghost-extract-api-key-5.19.0.tgz +0 -0
  15. package/components/tryghost-html-to-plaintext-5.19.0.tgz +0 -0
  16. package/components/tryghost-job-manager-5.19.0.tgz +0 -0
  17. package/components/tryghost-link-redirects-5.19.0.tgz +0 -0
  18. package/components/tryghost-link-replacer-5.19.0.tgz +0 -0
  19. package/components/tryghost-link-tracking-5.19.0.tgz +0 -0
  20. package/components/tryghost-magic-link-5.19.0.tgz +0 -0
  21. package/components/tryghost-mailgun-client-5.19.0.tgz +0 -0
  22. package/components/tryghost-member-analytics-service-5.19.0.tgz +0 -0
  23. package/components/tryghost-member-attribution-5.19.0.tgz +0 -0
  24. package/components/tryghost-member-events-5.19.0.tgz +0 -0
  25. package/components/tryghost-members-analytics-ingress-5.19.0.tgz +0 -0
  26. package/components/tryghost-members-api-5.19.0.tgz +0 -0
  27. package/components/tryghost-members-csv-5.19.0.tgz +0 -0
  28. package/components/tryghost-members-events-service-5.19.0.tgz +0 -0
  29. package/components/tryghost-members-importer-5.19.0.tgz +0 -0
  30. package/components/tryghost-members-offers-5.19.0.tgz +0 -0
  31. package/components/tryghost-members-payments-5.19.0.tgz +0 -0
  32. package/components/tryghost-members-ssr-5.19.0.tgz +0 -0
  33. package/components/tryghost-members-stripe-service-5.19.0.tgz +0 -0
  34. package/components/tryghost-minifier-5.19.0.tgz +0 -0
  35. package/components/tryghost-mw-api-version-mismatch-5.19.0.tgz +0 -0
  36. package/components/tryghost-mw-cache-control-5.19.0.tgz +0 -0
  37. package/components/{tryghost-mw-error-handler-5.17.2.tgz → tryghost-mw-error-handler-5.19.0.tgz} +0 -0
  38. package/components/tryghost-mw-session-from-token-5.19.0.tgz +0 -0
  39. package/components/tryghost-mw-update-user-last-seen-5.19.0.tgz +0 -0
  40. package/components/tryghost-mw-vhost-5.19.0.tgz +0 -0
  41. package/components/tryghost-oembed-service-5.19.0.tgz +0 -0
  42. package/components/tryghost-package-json-5.19.0.tgz +0 -0
  43. package/components/tryghost-referrers-5.19.0.tgz +0 -0
  44. package/components/tryghost-security-5.19.0.tgz +0 -0
  45. package/components/tryghost-session-service-5.19.0.tgz +0 -0
  46. package/components/tryghost-settings-path-manager-5.19.0.tgz +0 -0
  47. package/components/tryghost-staff-service-5.19.0.tgz +0 -0
  48. package/components/tryghost-stats-service-5.19.0.tgz +0 -0
  49. package/components/tryghost-update-check-service-5.19.0.tgz +0 -0
  50. package/components/tryghost-verification-trigger-5.19.0.tgz +0 -0
  51. package/components/tryghost-version-notifications-data-service-5.19.0.tgz +0 -0
  52. package/content/themes/casper/assets/built/screen.css +1 -1
  53. package/content/themes/casper/assets/built/screen.css.map +1 -1
  54. package/content/themes/casper/assets/css/screen.css +18 -21
  55. package/content/themes/casper/package.json +1 -1
  56. package/core/boot.js +3 -1
  57. package/core/built/admin/assets/{chunk.143.3f2f5cbbd1ef4b1425d9.js → chunk.143.eaf838fbf1470f018bf3.js} +6 -6
  58. package/core/built/admin/assets/{chunk.174.37fefc669899f0fd0064.js → chunk.174.3a133d51d9b45097c101.js} +160 -159
  59. package/core/built/admin/assets/{chunk.178.8fa31be80e639cbd2df2.js → chunk.178.44dae8a74f7f9d606e06.js} +4 -4
  60. package/core/built/admin/assets/{chunk.579.a9bccec4d650a7be727a.js → chunk.613.f1d519ad47e7f9024263.js} +8962 -8890
  61. package/core/built/admin/assets/{chunk.579.a9bccec4d650a7be727a.js.LICENSE.txt → chunk.613.f1d519ad47e7f9024263.js.LICENSE.txt} +0 -0
  62. package/core/built/admin/assets/{ghost-3577dfa3f54651db5ae5b9fd3d9b2824.js → ghost-5ce6f5a730c83c91fc258b12c537ea35.js} +2905 -2866
  63. package/core/built/admin/assets/ghost-982146a4ada3a5af1981d1919ae01d08.css +1 -0
  64. package/core/built/admin/assets/ghost-dark-41929e4857de411a23597a9de49a4e4f.css +1 -0
  65. package/core/built/admin/assets/{vendor-733135cd6cbca8126c6fa223d63a5bf3.css → vendor-3e6947aa681f0fb82b193090e520dc73.css} +24 -92
  66. package/core/built/admin/assets/{vendor-325e038b8609f0979f6578cae7a87f9e.js → vendor-5c7d7063620bec13668c4370145cd4b4.js} +561 -555
  67. package/core/built/admin/index.html +7 -7
  68. package/core/frontend/helpers/t.js +12 -0
  69. package/core/frontend/helpers/tpl/content-cta.hbs +1 -1
  70. package/core/frontend/public/ghost.min.css +1 -1
  71. package/core/frontend/public/robots.txt +1 -0
  72. package/core/frontend/services/sitemap/handler.js +1 -1
  73. package/core/frontend/services/sitemap/index-generator.js +1 -3
  74. package/core/frontend/web/middleware/static-theme.js +2 -0
  75. package/core/server/api/endpoints/feedback-members.js +23 -0
  76. package/core/server/api/endpoints/index.js +5 -1
  77. package/core/server/api/endpoints/utils/serializers/input/posts.js +3 -3
  78. package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +14 -13
  79. package/core/server/data/exporter/table-lists.js +3 -1
  80. package/core/server/data/importer/handlers/json.js +21 -23
  81. package/core/server/data/importer/importers/data/base.js +1 -1
  82. package/core/server/data/migrations/versions/4.0/05-add-members-subscribe-events-table.js +1 -1
  83. package/core/server/data/migrations/versions/4.0/06-populate-members-subscribe-events-table.js +1 -1
  84. package/core/server/data/migrations/versions/4.0/11-add-members-paid-subscription-events-table.js +1 -1
  85. package/core/server/data/migrations/versions/4.0/13-add-members-payment-events-table.js +1 -1
  86. package/core/server/data/migrations/versions/4.0/17-populate-members-status-events-table.js +1 -1
  87. package/core/server/data/migrations/versions/4.0/22-solve-orphaned-webhooks.js +1 -1
  88. package/core/server/data/migrations/versions/4.0/25-populate-members-paid-subscription-events-table.js +1 -1
  89. package/core/server/data/migrations/versions/4.11/02-add-email-verification-required-setting.js +1 -1
  90. package/core/server/data/migrations/versions/4.12/01-add-email-only-column-to-posts-meta-table.js +1 -1
  91. package/core/server/data/migrations/versions/4.3/03-add-default-product.js +1 -1
  92. package/core/server/data/migrations/versions/4.3/04-attach-members-to-product.js +1 -1
  93. package/core/server/data/migrations/versions/4.3/06-add-stripe-prices-table.js +2 -2
  94. package/core/server/data/migrations/versions/4.3/08-migrate-members-signup-setting.js +1 -1
  95. package/core/server/data/migrations/versions/4.33/2022-01-14-11-51-add-default-free-tier.js +1 -1
  96. package/core/server/data/migrations/versions/4.42/2022-03-21-17-17-add.js +2 -2
  97. package/core/server/data/migrations/versions/4.43/2022-03-28-19-26-recreate-newsletter-table.js +5 -5
  98. package/core/server/data/migrations/versions/4.46/2022-04-13-13-00-add-default-newsletter.js +1 -1
  99. package/core/server/data/migrations/versions/4.46/2022-04-20-08-39-map-subscribers-to-default-newsletter.js +1 -1
  100. package/core/server/data/migrations/versions/4.7/03-add-labs-setting.js +1 -1
  101. package/core/server/data/migrations/versions/4.8/03-add-default-product-portal-products.js +1 -1
  102. package/core/server/data/migrations/versions/4.8/04-migrate-show-newsletter-header-setting.js +1 -1
  103. package/core/server/data/migrations/versions/5.0/2022-05-06-13-22-add-frontend-integration.js +1 -1
  104. package/core/server/data/migrations/versions/5.17/2022-09-29-12-39-add-track-clicks-column-to-emails.js +2 -2
  105. package/core/server/data/migrations/versions/5.19/2022-09-02-20-25-add-columns-to-products-table.js +19 -0
  106. package/core/server/data/migrations/versions/5.19/2022-09-02-20-52-backfill-new-product-columns.js +37 -0
  107. package/core/server/data/migrations/versions/5.19/2022-10-10-06-58-add-subscriptions-table.js +19 -0
  108. package/core/server/data/migrations/versions/5.19/2022-10-10-10-05-add-members-feedback-table.js +10 -0
  109. package/core/server/data/migrations/versions/5.19/2022-10-11-10-38-add-feedback-enabled-column-to-newsletters.js +7 -0
  110. package/core/server/data/schema/commands.js +3 -3
  111. package/core/server/data/schema/fixtures/fixtures.json +4 -1
  112. package/core/server/data/schema/schema.js +90 -24
  113. package/core/server/data/schema/validator.js +1 -1
  114. package/core/server/models/base/bookshelf.js +3 -4
  115. package/core/server/models/base/plugins/data-manipulation.js +1 -1
  116. package/core/server/models/base/plugins/events.js +1 -1
  117. package/core/server/models/base/utils.js +1 -1
  118. package/core/server/models/member-feedback.js +22 -0
  119. package/core/server/models/newsletter.js +3 -2
  120. package/core/server/models/post.js +24 -0
  121. package/core/server/models/settings.js +1 -1
  122. package/core/server/models/user.js +1 -1
  123. package/core/server/services/audience-feedback/FeedbackRepository.js +67 -0
  124. package/core/server/services/audience-feedback/index.js +33 -0
  125. package/core/server/services/bulk-email/bulk-email-processor.js +7 -1
  126. package/core/server/services/mail/GhostMailer.js +17 -1
  127. package/core/server/services/mega/feedback-buttons.js +69 -0
  128. package/core/server/services/mega/mega.js +1 -1
  129. package/core/server/services/mega/post-email-serializer.js +24 -4
  130. package/core/server/services/mega/template.js +3 -0
  131. package/core/server/services/members/middleware.js +40 -0
  132. package/core/server/services/notifications/notifications.js +1 -1
  133. package/core/server/services/settings/settings-service.js +1 -1
  134. package/core/server/services/themes/storage.js +1 -1
  135. package/core/server/services/webhooks/serialize.js +1 -1
  136. package/core/server/web/admin/app.js +2 -0
  137. package/core/server/web/api/endpoints/admin/routes.js +1 -2
  138. package/core/server/web/members/app.js +12 -0
  139. package/core/shared/config/defaults.json +1 -1
  140. package/core/shared/labs.js +6 -4
  141. package/package.json +117 -117
  142. package/yarn.lock +4867 -1572
  143. package/components/tryghost-adapter-manager-5.17.2.tgz +0 -0
  144. package/components/tryghost-api-version-compatibility-service-5.17.2.tgz +0 -0
  145. package/components/tryghost-bootstrap-socket-5.17.2.tgz +0 -0
  146. package/components/tryghost-constants-5.17.2.tgz +0 -0
  147. package/components/tryghost-domain-events-5.17.2.tgz +0 -0
  148. package/components/tryghost-email-analytics-provider-mailgun-5.17.2.tgz +0 -0
  149. package/components/tryghost-email-analytics-service-5.17.2.tgz +0 -0
  150. package/components/tryghost-email-content-generator-5.17.2.tgz +0 -0
  151. package/components/tryghost-express-dynamic-redirects-5.17.2.tgz +0 -0
  152. package/components/tryghost-extract-api-key-5.17.2.tgz +0 -0
  153. package/components/tryghost-html-to-plaintext-5.17.2.tgz +0 -0
  154. package/components/tryghost-job-manager-5.17.2.tgz +0 -0
  155. package/components/tryghost-link-redirects-5.17.2.tgz +0 -0
  156. package/components/tryghost-link-replacer-5.17.2.tgz +0 -0
  157. package/components/tryghost-link-tracking-5.17.2.tgz +0 -0
  158. package/components/tryghost-magic-link-5.17.2.tgz +0 -0
  159. package/components/tryghost-mailgun-client-5.17.2.tgz +0 -0
  160. package/components/tryghost-member-analytics-service-5.17.2.tgz +0 -0
  161. package/components/tryghost-member-attribution-5.17.2.tgz +0 -0
  162. package/components/tryghost-member-events-5.17.2.tgz +0 -0
  163. package/components/tryghost-members-analytics-ingress-5.17.2.tgz +0 -0
  164. package/components/tryghost-members-api-5.17.2.tgz +0 -0
  165. package/components/tryghost-members-csv-5.17.2.tgz +0 -0
  166. package/components/tryghost-members-events-service-5.17.2.tgz +0 -0
  167. package/components/tryghost-members-importer-5.17.2.tgz +0 -0
  168. package/components/tryghost-members-offers-5.17.2.tgz +0 -0
  169. package/components/tryghost-members-payments-5.17.2.tgz +0 -0
  170. package/components/tryghost-members-ssr-5.17.2.tgz +0 -0
  171. package/components/tryghost-members-stripe-service-5.17.2.tgz +0 -0
  172. package/components/tryghost-minifier-5.17.2.tgz +0 -0
  173. package/components/tryghost-mw-api-version-mismatch-5.17.2.tgz +0 -0
  174. package/components/tryghost-mw-cache-control-5.17.2.tgz +0 -0
  175. package/components/tryghost-mw-session-from-token-5.17.2.tgz +0 -0
  176. package/components/tryghost-mw-update-user-last-seen-5.17.2.tgz +0 -0
  177. package/components/tryghost-mw-vhost-5.17.2.tgz +0 -0
  178. package/components/tryghost-oembed-service-5.17.2.tgz +0 -0
  179. package/components/tryghost-package-json-5.17.2.tgz +0 -0
  180. package/components/tryghost-referrers-5.17.2.tgz +0 -0
  181. package/components/tryghost-security-5.17.2.tgz +0 -0
  182. package/components/tryghost-session-service-5.17.2.tgz +0 -0
  183. package/components/tryghost-settings-path-manager-5.17.2.tgz +0 -0
  184. package/components/tryghost-staff-service-5.17.2.tgz +0 -0
  185. package/components/tryghost-stats-service-5.17.2.tgz +0 -0
  186. package/components/tryghost-update-check-service-5.17.2.tgz +0 -0
  187. package/components/tryghost-verification-trigger-5.17.2.tgz +0 -0
  188. package/components/tryghost-version-notifications-data-service-5.17.2.tgz +0 -0
  189. package/core/built/admin/assets/ghost-597fb8e8b1b91dd0ac4d9f2d75bd67fb.css +0 -1
  190. package/core/built/admin/assets/ghost-dark-e4ccecd9903d35d360d71fe859cbb3bf.css +0 -1
@@ -0,0 +1,37 @@
1
+ const logging = require('@tryghost/logging');
2
+
3
+ const {createTransactionalMigration} = require('../../utils');
4
+
5
+ module.exports = createTransactionalMigration(
6
+ async function up(knex) {
7
+ const rows = await knex('products as t') // eslint-disable-line no-restricted-syntax
8
+ .select(
9
+ 't.id as id',
10
+ 'mp.amount as monthly_price',
11
+ 'yp.amount as yearly_price',
12
+ knex.raw('coalesce(yp.currency, mp.currency) as currency')
13
+ )
14
+ .leftJoin('stripe_prices AS mp', 't.monthly_price_id', 'mp.id')
15
+ .leftJoin('stripe_prices AS yp', 't.yearly_price_id', 'yp.id')
16
+ .where('t.type', 'paid');
17
+
18
+ if (!rows.length) {
19
+ logging.info('Did not find any active paid Tiers');
20
+ return;
21
+ } else {
22
+ logging.info(`Updating ${rows.length} Tiers with price and currency information`);
23
+ }
24
+
25
+ for (const row of rows) { // eslint-disable-line no-restricted-syntax
26
+ await knex('products').update(row).where('id', row.id);
27
+ }
28
+ },
29
+ async function down(knex) {
30
+ logging.info('Removing currency and price information for all tiers');
31
+ await knex('products').update({
32
+ currency: null,
33
+ monthly_price: null,
34
+ yearly_price: null
35
+ });
36
+ }
37
+ );
@@ -0,0 +1,19 @@
1
+ const {addTable} = require('../../utils');
2
+
3
+ module.exports = addTable('subscriptions', {
4
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
5
+ type: {type: 'string', maxlength: 50, nullable: false},
6
+ status: {type: 'string', maxlength: 50, nullable: false},
7
+ member_id: {type: 'string', maxlength: 24, nullable: false, unique: false, references: 'members.id', cascadeDelete: true},
8
+ tier_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'products.id'},
9
+ cadence: {type: 'string', maxlength: 50, nullable: true},
10
+ currency: {type: 'string', maxlength: 50, nullable: true},
11
+ amount: {type: 'integer', nullable: true},
12
+ payment_provider: {type: 'string', maxlength: 50, nullable: true},
13
+ payment_subscription_url: {type: 'string', maxlength: 2000, nullable: true},
14
+ payment_user_url: {type: 'string', maxlength: 2000, nullable: true},
15
+ offer_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'offers.id'},
16
+ expires_at: {type: 'dateTime', nullable: true},
17
+ created_at: {type: 'dateTime', nullable: false},
18
+ updated_at: {type: 'dateTime', nullable: true}
19
+ });
@@ -0,0 +1,10 @@
1
+ const {addTable} = require('../../utils');
2
+
3
+ module.exports = addTable('members_feedback', {
4
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
5
+ score: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0},
6
+ member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
7
+ post_id: {type: 'string', maxlength: 24, nullable: false, references: 'posts.id', cascadeDelete: true},
8
+ created_at: {type: 'dateTime', nullable: false},
9
+ updated_at: {type: 'dateTime', nullable: true}
10
+ });
@@ -0,0 +1,7 @@
1
+ const {createAddColumnMigration} = require('../../utils');
2
+
3
+ module.exports = createAddColumnMigration('newsletters', 'feedback_enabled', {
4
+ type: 'boolean',
5
+ nullable: false,
6
+ defaultTo: false
7
+ });
@@ -126,7 +126,7 @@ async function dropColumn(tableName, column, transaction = db.knex, columnSpec =
126
126
  * Adds an unique index to a table over the given columns.
127
127
  *
128
128
  * @param {string} tableName - name of the table to add unique constraint to
129
- * @param {string|[string]} columns - column(s) to form unique constraint with
129
+ * @param {string|string[]} columns - column(s) to form unique constraint with
130
130
  * @param {import('knex')} transaction - connection object containing knex reference
131
131
  */
132
132
  async function addUnique(tableName, columns, transaction = db.knex) {
@@ -153,7 +153,7 @@ async function addUnique(tableName, columns, transaction = db.knex) {
153
153
  * Drops a unique key constraint from a table.
154
154
  *
155
155
  * @param {string} tableName - name of the table to drop unique constraint from
156
- * @param {string|[string]} columns - column(s) unique constraint was formed
156
+ * @param {string|string[]} columns - column(s) unique constraint was formed
157
157
  * @param {import('knex')} transaction - connection object containing knex reference
158
158
  */
159
159
  async function dropUnique(tableName, columns, transaction = db.knex) {
@@ -327,7 +327,7 @@ async function hasPrimaryKeySQLite(tableName, transaction = db.knex) {
327
327
  * Adds an primary key index to a table over the given columns.
328
328
  *
329
329
  * @param {string} tableName - name of the table to add primaykey constraint to
330
- * @param {string|[string]} columns - column(s) to form primary key constraint with
330
+ * @param {string|string[]} columns - column(s) to form primary key constraint with
331
331
  * @param {import('knex')} transaction - connection object containing knex reference
332
332
  */
333
333
  async function addPrimaryKey(tableName, columns, transaction = db.knex) {
@@ -15,7 +15,10 @@
15
15
  "slug": "default-product",
16
16
  "type": "paid",
17
17
  "active": true,
18
- "visibility": "public"
18
+ "visibility": "public",
19
+ "currency": "usd",
20
+ "monthly_price": 500,
21
+ "yearly_price": 5000
19
22
  }
20
23
  ]
21
24
  },
@@ -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