ghost 6.54.0 → 6.54.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 (238) hide show
  1. package/components/ghost-storage-base-3.0.0.tgz +0 -0
  2. package/components/tryghost-adapter-base-cache-0.2.0.tgz +0 -0
  3. package/components/tryghost-adapter-base-scheduling-0.2.0.tgz +0 -0
  4. package/components/tryghost-adapter-base-sso-0.1.1.tgz +0 -0
  5. package/components/tryghost-kg-default-nodes-2.1.3.tgz +0 -0
  6. package/core/built/admin/assets/Growth-BTP1LthE.js +3 -0
  7. package/core/built/admin/assets/Newsletters-CSEX0myc.js +1 -0
  8. package/core/built/admin/assets/Overview-BjACEsJF.js +1 -0
  9. package/core/built/admin/assets/Web-CxxxjCcv.js +2 -0
  10. package/core/built/admin/assets/{account-migration-DLvVA5yq.js → account-migration-BklF2I_I.js} +1 -1
  11. package/core/built/admin/assets/activitypub/{account-migration-DHbZcxGM.js → account-migration-CNovr6W-.js} +4 -4
  12. package/core/built/admin/assets/activitypub/activitypub.js +2 -2
  13. package/core/built/admin/assets/activitypub/{bluesky-sharing-zfxUey_L.js → bluesky-sharing-Ceslb1_M.js} +4 -4
  14. package/core/built/admin/assets/activitypub/{dist-BqkDUQxJ2.js → dist-ByFmDSNP2.js} +64 -63
  15. package/core/built/admin/assets/activitypub/{dist-Ba_QxHHl.js → dist-DHAjBcV3.js} +2 -2
  16. package/core/built/admin/assets/activitypub/{domain-CmwSZLHK.js → domain-C1qQvJ8C.js} +5 -5
  17. package/core/built/admin/assets/activitypub/{edit-profile-CXdwNmVG.js → edit-profile-BcH-jEfM.js} +3 -3
  18. package/core/built/admin/assets/activitypub/{explore-DfbjzGM1.js → explore-AxwBh9dI.js} +3 -3
  19. package/core/built/admin/assets/activitypub/{feed-DSEBJOqf.js → feed-BaOlbh7a.js} +3 -3
  20. package/core/built/admin/assets/activitypub/{field-CY7XVcx6.js → field-B7mYYQpN.js} +2 -2
  21. package/core/built/admin/assets/activitypub/{inbox-DtQ0kytT.js → inbox-vXez-5uJ.js} +4 -4
  22. package/core/built/admin/assets/activitypub/{label-Dw96GQ7u.js → label-Chv5T8bw.js} +2 -2
  23. package/core/built/admin/assets/activitypub/layout-CziktJXo.js +1 -0
  24. package/core/built/admin/assets/activitypub/{moderation-Dg_-I-8C.js → moderation-DtSM2i1I.js} +4 -4
  25. package/core/built/admin/assets/activitypub/{note-Cy1-uKMY.js → note-Dckp4S-2.js} +3 -3
  26. package/core/built/admin/assets/activitypub/{notifications-B_fvoLIM.js → notifications-CG5T22vg.js} +3 -3
  27. package/core/built/admin/assets/activitypub/{preferences-ByJseZp6.js → preferences-D_799jdl.js} +6 -6
  28. package/core/built/admin/assets/activitypub/{profile-BFtbnIjz.js → profile-D2JfXBQl.js} +6 -6
  29. package/core/built/admin/assets/activitypub/{routes-Dh3xXfce.js → routes-CCHe2dQp.js} +28 -25
  30. package/core/built/admin/assets/activitypub/{settings-DTgqJPxM.js → settings-kx-9Enkz.js} +3 -3
  31. package/core/built/admin/assets/activitypub/{site-CdL9ZLqm.js → site-x4_IjVJ0.js} +2 -2
  32. package/core/built/admin/assets/activitypub/{tabs-ogqwoBlb.js → tabs-CyPRQQ1N.js} +3 -3
  33. package/core/built/admin/assets/activitypub/{use-reply-chain-data-CxonteFD.js → use-reply-chain-data-BAB6fNGt.js} +2 -2
  34. package/core/built/admin/assets/{audience-BKRdsFmy.js → audience-X_XsZivI.js} +1 -1
  35. package/core/built/admin/assets/{automated-emails-Cn87L5Md.js → automated-emails-of1MvVsv.js} +1 -1
  36. package/core/built/admin/assets/{automations-BP_aHamJ.js → automations-DRFjW5fw.js} +1 -1
  37. package/core/built/admin/assets/{bluesky-sharing-Bh36XPh4.js → bluesky-sharing-BbbjWdzs.js} +1 -1
  38. package/core/built/admin/assets/{breadcrumb-D34YWnaw.js → breadcrumb-B-M9Ip_2.js} +1 -1
  39. package/core/built/admin/assets/{chart-helpers-DbklaJw9.js → chart-helpers-xiSsW8r0.js} +1 -1
  40. package/core/built/admin/assets/checkbox-EhJKHFdZ.js +1 -0
  41. package/core/built/admin/assets/{chunk.962.b73442b07be930bb5d85.js → chunk.211.6f7328d63c41536451b3.js} +737 -559
  42. package/core/built/admin/assets/{chunk.962.b73442b07be930bb5d85.js.LICENSE.txt → chunk.211.6f7328d63c41536451b3.js.LICENSE.txt} +2 -0
  43. package/core/built/admin/assets/{chunk.589.096f4d0bb775e5229aa9.js → chunk.361.3cdb584b973959e52ccc.js} +3 -3
  44. package/core/built/admin/assets/chunk.524.8365cd9280405c0f6216.js +25 -0
  45. package/core/built/admin/assets/chunk.582.61d078937e6102769f2c.js +27 -0
  46. package/core/built/admin/assets/code-editor-view-CZtJFGhR.js +1 -0
  47. package/core/built/admin/assets/comments-CvyXxOyN.js +1 -0
  48. package/core/built/admin/assets/{date-range-select-B5vSsrpx.js → date-range-select-CTlyo8zW.js} +1 -1
  49. package/core/built/admin/assets/{disabled-sources-indicator-NkOIoMue.js → disabled-sources-indicator-qs2GfLvl.js} +1 -1
  50. package/core/built/admin/assets/dist-2Tnz2Yfq.js +1 -0
  51. package/core/built/admin/assets/dist-CjY-f5Z1.js +1 -0
  52. package/core/built/admin/assets/dist-DIC8yz5C.js +1 -0
  53. package/core/built/admin/assets/{domain-B7_n5nfx.js → domain-CPNXKVOV.js} +1 -1
  54. package/core/built/admin/assets/dropzone-BmCXnw6J.js +5 -0
  55. package/core/built/admin/assets/{edit-profile-rvDGrS8J.js → edit-profile-SGrJve5M.js} +1 -1
  56. package/core/built/admin/assets/editor-BOG2YPUm.js +8 -0
  57. package/core/built/admin/assets/{explore-BZAZOg5X.js → explore-CUYLv9G9.js} +1 -1
  58. package/core/built/admin/assets/{feed-CFV8xDw9.js → feed-CPdJMttL.js} +1 -1
  59. package/core/built/admin/assets/{field-BW-M2Zwp.js → field-D0XISCK2.js} +1 -1
  60. package/core/built/admin/assets/{filter-sources-Fx8wiYYw.js → filter-sources-Bieps75g.js} +1 -1
  61. package/core/built/admin/assets/{filters-CGX_pf2N.js → filters-CuB-A7ma.js} +1 -1
  62. package/core/built/admin/assets/filters-bXkkWro8.js +1 -0
  63. package/core/built/admin/assets/{ghost-b1dce91ad3232cc259ee5fbbf9526243.js → ghost-05f9a7182ed56903dba05878a5b8422b.js} +290 -289
  64. package/core/built/admin/assets/{gift-link-modal-Bk8qpnMw.js → gift-link-modal-su8MsRdg.js} +1 -1
  65. package/core/built/admin/assets/{growth-Ck2s3JjR.js → growth-Ba6S5FQX.js} +1 -1
  66. package/core/built/admin/assets/{inbox-BRp1_aaC.js → inbox-Xx1CdnfO.js} +8 -8
  67. package/core/built/admin/assets/index-D8PbzuT9.css +1 -0
  68. package/core/built/admin/assets/index-ukW_0Uie.js +67 -0
  69. package/core/built/admin/assets/input-group-CGOXcy4l.js +1 -0
  70. package/core/built/admin/assets/input-surface-BpAjZXY5.js +1 -0
  71. package/core/built/admin/assets/{koenig-lexical-CM7UFNdL.js → koenig-lexical-B61IALf5.js} +1 -1
  72. package/core/built/admin/assets/{kpi-card-CIGyijjx.js → kpi-card-BlZaG1Lo.js} +1 -1
  73. package/core/built/admin/assets/{kpi-card-BrGH6FsT.js → kpi-card-DCdlyaSS.js} +1 -1
  74. package/core/built/admin/assets/{kpi-helpers-_HRRsklE.js → kpi-helpers-DpAuS1Nb.js} +1 -1
  75. package/core/built/admin/assets/{kpi-tabs-BSvh9eoq.js → kpi-tabs-Dme44bQS.js} +1 -1
  76. package/core/built/admin/assets/{label-BOR9XdKl.js → label-dSs5aepE.js} +1 -1
  77. package/core/built/admin/assets/{label-picker-BdNAWuUX.js → label-picker-Uq91atex.js} +1 -1
  78. package/core/built/admin/assets/layout-WUcz6tu1.js +1 -0
  79. package/core/built/admin/assets/{member-custom-fields-DvFyamLu.js → member-custom-fields-NCzrMuba.js} +1 -1
  80. package/core/built/admin/assets/member-detail-DPWDtYnN.js +1 -0
  81. package/core/built/admin/assets/{members-DsPxYAVm.js → members-B9rO7qr4.js} +2 -2
  82. package/core/built/admin/assets/modals-CxRaan1J.js +31 -0
  83. package/core/built/admin/assets/{moderation-Dlp7M5sa.js → moderation-D8zon60E.js} +1 -1
  84. package/core/built/admin/assets/{multi-select-combobox-CH1o8XXQ.js → multi-select-combobox-1wYm3nle.js} +1 -1
  85. package/core/built/admin/assets/newsletter-DbYMRxRS.js +1 -0
  86. package/core/built/admin/assets/{note-BrAZ7Z0o.js → note-PiWPnnrh.js} +1 -1
  87. package/core/built/admin/assets/{notifications-BnbHLOPC.js → notifications-DEgqqB9G.js} +1 -1
  88. package/core/built/admin/assets/{onboarding-route-eU146kuA.js → onboarding-route-BzF6HeVn.js} +1 -1
  89. package/core/built/admin/assets/{overview-BpbaK-xu.js → overview-DixLeIqY.js} +1 -1
  90. package/core/built/admin/assets/{page-header-C0jVxtS8.js → page-header-op0eN36I.js} +1 -1
  91. package/core/built/admin/assets/{post-analytics-Iw7dk1mT.js → post-analytics-WwRDEnrV.js} +1 -1
  92. package/core/built/admin/assets/post-analytics-header-DsMoLx0R.js +1 -0
  93. package/core/built/admin/assets/{post-analytics-provider-BDqcfBor.js → post-analytics-provider-BJCPLMIb.js} +1 -1
  94. package/core/built/admin/assets/{post-share-modal-DBfKl2GZ.js → post-share-modal-Dx-MEjhI.js} +1 -1
  95. package/core/built/admin/assets/preferences-D7-j6A4C.js +5 -0
  96. package/core/built/admin/assets/profile-Dl69QPp0.js +1 -0
  97. package/core/built/admin/assets/select-C1qDZnW4.js +1 -0
  98. package/core/built/admin/assets/settings-BbmC_Iqq.js +4 -0
  99. package/core/built/admin/assets/{settings-Bz3RZRmE.js → settings-H6HxFgfI.js} +1 -1
  100. package/core/built/admin/assets/shade-app-B3heOAfz-CGXQwWeO.js +3 -0
  101. package/core/built/admin/assets/{share-modal-CkdTTMJd.js → share-modal-BJCadJyp.js} +1 -1
  102. package/core/built/admin/assets/social-urls-BPL1nADp.js +8 -0
  103. package/core/built/admin/assets/{sort-button-DZp9Y2r1.js → sort-button-DyYlfYp7.js} +1 -1
  104. package/core/built/admin/assets/{source-icon-CHr9qJ2Y.js → source-icon-_ubRDbg_.js} +1 -1
  105. package/core/built/admin/assets/{stats-DGoXZxWE.js → stats-BQZMGMvn.js} +1 -1
  106. package/core/built/admin/assets/{stats-view-D2-1L0ms.js → stats-view-Doei-qCX.js} +1 -1
  107. package/core/built/admin/assets/{step-1-RdahbwyG.js → step-1-IKiLBPoC.js} +1 -1
  108. package/core/built/admin/assets/{step-2-D83PeRwd.js → step-2-jJ41PsDN.js} +1 -1
  109. package/core/built/admin/assets/{step-3-p_Op4wv4.js → step-3-CaqFAEqf.js} +2 -2
  110. package/core/built/admin/assets/switch-t-WyENU2.js +1 -0
  111. package/core/built/admin/assets/{table-B8LgTQJZ.js → table-DxUL78jU.js} +1 -1
  112. package/core/built/admin/assets/{tabs-D3ZOYkM7.js → tabs-BQcJQ1ke.js} +1 -1
  113. package/core/built/admin/assets/{tags-B4l891z2.js → tags-CNujGwkT.js} +1 -1
  114. package/core/built/admin/assets/textarea-Bjccvh_E.js +1 -0
  115. package/core/built/admin/assets/{toggle-group-DNXDkAwf.js → toggle-group-BeEpQ69p.js} +1 -1
  116. package/core/built/admin/assets/{topic-filter-DZGjEcz3.js → topic-filter-Bb5MOD-1.js} +1 -1
  117. package/core/built/admin/assets/{use-analytics-data-CaT8RirX.js → use-analytics-data-Cds6XDjg.js} +1 -1
  118. package/core/built/admin/assets/use-filter-params-DjXkhO54.js +1 -0
  119. package/core/built/admin/assets/{use-growth-stats-9Db2CuJe.js → use-growth-stats-D8uf98-G.js} +1 -1
  120. package/core/built/admin/assets/{use-post-referrers-BV54YcQS.js → use-post-referrers-CRtgyuXB.js} +1 -1
  121. package/core/built/admin/assets/{use-reply-chain-data-DuYZGDan.js → use-reply-chain-data-DbKYX50v.js} +1 -1
  122. package/core/built/admin/assets/{use-simple-pagination-8fTua9Nt.js → use-simple-pagination-DC01kpo0.js} +1 -1
  123. package/core/built/admin/assets/{use-tinybird-query-Dz156HvZ.js → use-tinybird-query-C7bQRJWw.js} +1 -1
  124. package/core/built/admin/assets/{use-tinybird-token-C9RUdL_n.js → use-tinybird-token-Az4tam7_.js} +1 -1
  125. package/core/built/admin/assets/{virtual-list-BCvP0YF-.js → virtual-list-Dhuh9fGx.js} +1 -1
  126. package/core/built/admin/assets/web-LtHNiNBV.js +1 -0
  127. package/core/built/admin/index.html +11 -7
  128. package/core/frontend/services/proxy.js +11 -1
  129. package/core/server/adapters/storage/LocalFilesStorage.js +13 -11
  130. package/core/server/adapters/storage/LocalFilesStorage.ts +24 -0
  131. package/core/server/adapters/storage/LocalImagesStorage.js +14 -12
  132. package/core/server/adapters/storage/LocalImagesStorage.ts +24 -0
  133. package/core/server/adapters/storage/LocalMediaStorage.js +13 -11
  134. package/core/server/adapters/storage/LocalMediaStorage.ts +24 -0
  135. package/core/server/adapters/storage/LocalStorageBase.js +107 -167
  136. package/core/server/adapters/storage/LocalStorageBase.ts +342 -0
  137. package/core/server/adapters/storage/utils.js +36 -50
  138. package/core/server/adapters/storage/utils.ts +60 -0
  139. package/core/server/api/endpoints/db.js +6 -0
  140. package/core/server/api/endpoints/members.js +25 -28
  141. package/core/server/api/endpoints/pages.js +6 -1
  142. package/core/server/api/endpoints/posts.js +11 -2
  143. package/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +4 -1
  144. package/core/server/api/endpoints/utils/serializers/output/members.js +18 -87
  145. package/core/server/data/exporter/export-filename.js +2 -1
  146. package/core/server/data/importer/importers/data/revue-subscriber-importer.js +6 -10
  147. package/core/server/data/importer/importers/json-to-html.js +24 -4
  148. package/core/server/lib/image/image-size.js +3 -17
  149. package/core/server/lib/image/image-utils.js +15 -1
  150. package/core/server/lib/request-external.js +1 -1
  151. package/core/server/models/user.js +46 -70
  152. package/core/server/services/auth/session/session-service.js +28 -1
  153. package/core/server/services/automations/automations-api.js +6 -6
  154. package/core/server/services/automations/automations-api.ts +6 -6
  155. package/core/server/services/automations/automations-repository.ts +1 -0
  156. package/core/server/services/automations/database-automations-repository.js +7 -2
  157. package/core/server/services/automations/database-automations-repository.ts +8 -2
  158. package/core/server/services/comments/comments-service-emails.js +4 -1
  159. package/core/server/services/members/{importer → import-export}/config.js +4 -9
  160. package/core/server/services/members/{importer → import-export}/config.ts +4 -9
  161. package/core/server/services/members/{csv → import-export/csv}/index.js +3 -3
  162. package/core/server/services/members/import-export/csv/index.ts +2 -0
  163. package/core/server/services/members/import-export/csv/parse.js +64 -0
  164. package/core/server/services/members/import-export/csv/parse.ts +69 -0
  165. package/core/server/services/members/import-export/csv/serialize.js +13 -0
  166. package/core/server/services/members/import-export/csv/serialize.ts +18 -0
  167. package/core/server/services/members/import-export/export/exporter.js +202 -0
  168. package/core/server/services/members/import-export/export/exporter.ts +325 -0
  169. package/core/server/services/members/import-export/import/completion-email.js +65 -0
  170. package/core/server/services/members/import-export/import/completion-email.ts +119 -0
  171. package/core/server/services/members/import-export/import/importer.js +276 -0
  172. package/core/server/services/members/import-export/import/importer.ts +430 -0
  173. package/core/server/services/members/import-export/import/reader.js +32 -0
  174. package/core/server/services/members/import-export/import/reader.ts +33 -0
  175. package/core/server/services/members/import-export/import/row.js +33 -0
  176. package/core/server/services/members/import-export/import/row.ts +52 -0
  177. package/core/server/services/members/import-export/import/spool.js +30 -0
  178. package/core/server/services/members/import-export/import/spool.ts +38 -0
  179. package/core/server/services/members/import-export/index.js +90 -0
  180. package/core/server/services/members/import-export/index.ts +118 -0
  181. package/core/server/services/members/members-api/controllers/router-controller.js +7 -0
  182. package/core/server/services/members/service.js +17 -19
  183. package/core/server/services/posts/posts-exporter.js +6 -4
  184. package/core/server/services/posts/posts-service.js +2 -2
  185. package/core/server/services/themes/storage.js +9 -0
  186. package/core/server/services/themes/theme-storage.js +1 -1
  187. package/core/shared/config/defaults.json +2 -1
  188. package/core/shared/express.js +6 -1
  189. package/core/shared/labs.js +2 -1
  190. package/package.json +9 -9
  191. package/pnpm-lock.yaml +17 -17
  192. package/pnpm-workspace.yaml +7 -7
  193. package/components/tryghost-adapter-base-scheduling-0.1.1.tgz +0 -0
  194. package/core/built/admin/assets/Growth-CSaHoSVD.js +0 -3
  195. package/core/built/admin/assets/Newsletters-C5cs6FGo.js +0 -1
  196. package/core/built/admin/assets/Overview-j4NaeUNm.js +0 -1
  197. package/core/built/admin/assets/Web-_fD935OX.js +0 -2
  198. package/core/built/admin/assets/activitypub/layout-IdVq3lRy.js +0 -1
  199. package/core/built/admin/assets/checkbox-DA4-1UiI.js +0 -1
  200. package/core/built/admin/assets/chunk.524.45e0875f41b71e46e849.js +0 -25
  201. package/core/built/admin/assets/chunk.582.51defd4b009f0de8d4d5.js +0 -27
  202. package/core/built/admin/assets/code-editor-view-BKBrkffZ.js +0 -1
  203. package/core/built/admin/assets/comments-CzVNIT8b.js +0 -1
  204. package/core/built/admin/assets/dist-CT46_f_J.js +0 -1
  205. package/core/built/admin/assets/dropzone-DcuJEEUf.js +0 -5
  206. package/core/built/admin/assets/editor-BCiIyFjm.js +0 -8
  207. package/core/built/admin/assets/filters-DtcXMZDU.js +0 -1
  208. package/core/built/admin/assets/index-BBf502Wg.js +0 -69
  209. package/core/built/admin/assets/index-BCCZKxP2.css +0 -1
  210. package/core/built/admin/assets/input-group-DjuYnNCd.js +0 -1
  211. package/core/built/admin/assets/layout-Bw4GnxXp.js +0 -1
  212. package/core/built/admin/assets/member-detail-Cg2vTqvJ.js +0 -1
  213. package/core/built/admin/assets/modals-Bf_fyvHZ.js +0 -31
  214. package/core/built/admin/assets/newsletter-CQ5CAYUI.js +0 -1
  215. package/core/built/admin/assets/post-analytics-header-D9pJ4iNm.js +0 -1
  216. package/core/built/admin/assets/preferences-D4EANGoc.js +0 -5
  217. package/core/built/admin/assets/profile-dJ44Bn_W.js +0 -1
  218. package/core/built/admin/assets/select--2uVN9_-.js +0 -1
  219. package/core/built/admin/assets/settings-SD9bxIpt.js +0 -4
  220. package/core/built/admin/assets/social-urls-BvFZwYcE.js +0 -8
  221. package/core/built/admin/assets/switch-qp518gFQ.js +0 -1
  222. package/core/built/admin/assets/textarea-CTlb3Uu6.js +0 -1
  223. package/core/built/admin/assets/use-filter-params-DeQ8sX4N.js +0 -1
  224. package/core/built/admin/assets/web-DTZnnUGr.js +0 -1
  225. package/core/server/services/mail/templates/newsletter.html +0 -876
  226. package/core/server/services/members/csv/index.ts +0 -3
  227. package/core/server/services/members/csv/parse.js +0 -94
  228. package/core/server/services/members/csv/parse.ts +0 -111
  229. package/core/server/services/members/csv/types.js +0 -2
  230. package/core/server/services/members/csv/types.ts +0 -39
  231. package/core/server/services/members/csv/unparse.js +0 -69
  232. package/core/server/services/members/csv/unparse.ts +0 -79
  233. package/core/server/services/members/exporter/query.js +0 -233
  234. package/core/server/services/members/importer/index.js +0 -30
  235. package/core/server/services/members/importer/labels.js +0 -125
  236. package/core/server/services/members/importer/members-csv-importer.js +0 -520
  237. /package/core/server/services/members/{importer → import-export/import}/email-template.js +0 -0
  238. /package/core/server/services/members/{importer/members-csv-importer-stripe-utils.js → import-export/import/stripe-utils.js} +0 -0
@@ -65,6 +65,7 @@ interface ActionRow {
65
65
 
66
66
  type ActionStatsRow = {
67
67
  action_id: string;
68
+ email_clicked_count: number | null;
68
69
  email_sent_count: number | null;
69
70
  email_opened_count: number | null;
70
71
  };
@@ -1264,6 +1265,7 @@ async function loadActionStats(
1264
1265
  const rows: ActionStatsRow[] = await trx('automation_action_revisions')
1265
1266
  .select('action_id')
1266
1267
  .sum({
1268
+ email_clicked_count: 'email_clicked_count',
1267
1269
  email_sent_count: 'email_sent_count',
1268
1270
  email_opened_count: 'email_opened_count'
1269
1271
  })
@@ -1319,6 +1321,7 @@ function buildActionPayload(row: ActionRow, stats: AutomationEmailStats | null):
1319
1321
  }
1320
1322
 
1321
1323
  const EMPTY_EMAIL_STATS: AutomationEmailStats = {
1324
+ email_clicked_count: 0,
1322
1325
  email_sent_count: 0,
1323
1326
  email_opened_count: 0,
1324
1327
  opened_rate: null,
@@ -1326,16 +1329,19 @@ const EMPTY_EMAIL_STATS: AutomationEmailStats = {
1326
1329
  };
1327
1330
 
1328
1331
  function buildEmailStats(row: ActionStatsRow): AutomationEmailStats {
1332
+ const emailClickedCount = row.email_clicked_count ?? 0;
1329
1333
  const emailSentCount = row.email_sent_count ?? 0;
1330
1334
  const emailOpenedCount = row.email_opened_count ?? 0;
1331
1335
  return {
1336
+ email_clicked_count: emailClickedCount,
1332
1337
  email_sent_count: emailSentCount,
1333
1338
  email_opened_count: emailOpenedCount,
1334
1339
  opened_rate: emailSentCount
1335
1340
  ? Math.round(emailOpenedCount / emailSentCount * 100)
1336
1341
  : null,
1337
- // TODO(NY-1387) Populate clicked_rate once click tracking is implemented.
1338
- clicked_rate: null
1342
+ clicked_rate: emailSentCount
1343
+ ? Math.round(emailClickedCount / emailSentCount * 100)
1344
+ : null
1339
1345
  };
1340
1346
  }
1341
1347
 
@@ -26,7 +26,10 @@ class CommentsServiceEmails {
26
26
  */
27
27
  getPostUrl(post, commentId) {
28
28
  const postData = toPlain(post);
29
- const baseUrl = this.urlService.facade.getUrlForResource({...postData, type: 'posts'}, {absolute: true});
29
+ // Comments live on pages as well as posts; the URL service routes by
30
+ // the passed type, so a page typed 'posts' resolves to /404/.
31
+ const routerType = postData.type === 'page' ? 'pages' : 'posts';
32
+ const baseUrl = this.urlService.facade.getUrlForResource({...postData, type: routerType}, {absolute: true});
30
33
  return `${baseUrl}#ghost-comments-${commentId}`;
31
34
  }
32
35
 
@@ -2,15 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.resolveInlineThreshold = resolveInlineThreshold;
4
4
  const zod_1 = require("zod");
5
- /**
6
- * Resolving Ghost's config into the values the importer runs on.
7
- *
8
- * Config arrives untyped: defaults.json supplies the shipped value, and any
9
- * higher layer — an operator's config file, an environment variable, argv — can
10
- * replace it with anything at all. Turning that into something usable is a
11
- * config concern, so it happens here, at the boundary. The importer is handed an
12
- * answer and never learns where it came from.
13
- */
5
+ // Config arrives untyped: defaults.json ships a value, and any higher layer -- an
6
+ // operator's config file, an environment variable, argv -- can replace it with anything.
7
+ // Validating it into a usable number is a config concern, so it happens here at the
8
+ // boundary; the importer is handed a resolved value.
14
9
  // The row count at or below which an import is performed while the request is
15
10
  // still open. Zero is allowed and means every import is deferred. A numeric
16
11
  // string is accepted because a value supplied through an environment variable
@@ -1,14 +1,9 @@
1
1
  import {z} from 'zod';
2
2
 
3
- /**
4
- * Resolving Ghost's config into the values the importer runs on.
5
- *
6
- * Config arrives untyped: defaults.json supplies the shipped value, and any
7
- * higher layer — an operator's config file, an environment variable, argv — can
8
- * replace it with anything at all. Turning that into something usable is a
9
- * config concern, so it happens here, at the boundary. The importer is handed an
10
- * answer and never learns where it came from.
11
- */
3
+ // Config arrives untyped: defaults.json ships a value, and any higher layer -- an
4
+ // operator's config file, an environment variable, argv -- can replace it with anything.
5
+ // Validating it into a usable number is a config concern, so it happens here at the
6
+ // boundary; the importer is handed a resolved value.
12
7
 
13
8
  // The row count at or below which an import is performed while the request is
14
9
  // still open. Zero is allowed and means every import is deferred. A numeric
@@ -3,8 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.unparse = exports.parse = void 0;
6
+ exports.serialize = exports.parse = void 0;
7
7
  var parse_1 = require("./parse");
8
8
  Object.defineProperty(exports, "parse", { enumerable: true, get: function () { return __importDefault(parse_1).default; } });
9
- var unparse_1 = require("./unparse");
10
- Object.defineProperty(exports, "unparse", { enumerable: true, get: function () { return __importDefault(unparse_1).default; } });
9
+ var serialize_1 = require("./serialize");
10
+ Object.defineProperty(exports, "serialize", { enumerable: true, get: function () { return __importDefault(serialize_1).default; } });
@@ -0,0 +1,2 @@
1
+ export {default as parse, type Row} from './parse';
2
+ export {default as serialize} from './serialize';
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = parse;
7
+ const node_stream_1 = require("node:stream");
8
+ const papaparse_1 = __importDefault(require("papaparse"));
9
+ const fs_extra_1 = __importDefault(require("fs-extra"));
10
+ // A column named after an Object.prototype member (toString, __proto__, ...) is unsafe
11
+ // as a key: writing it would shadow a method or reach the prototype.
12
+ function isSafeColumnName(name) {
13
+ return !(name in Object.prototype);
14
+ }
15
+ // headerMapping renames the CSV's headers to the columns they emit under; unmapped
16
+ // columns carry through under their own name.
17
+ function parse(path, headerMapping) {
18
+ return new Promise(function (resolve, reject) {
19
+ const csvFileStream = fs_extra_1.default.createReadStream(path);
20
+ const csvParserStream = papaparse_1.default.parse(papaparse_1.default.NODE_STREAM_INPUT, {
21
+ header: true
22
+ });
23
+ const rows = [];
24
+ const parsedCSVStream = (0, node_stream_1.pipeline)(csvFileStream, csvParserStream, (err) => {
25
+ if (err) {
26
+ return reject(err);
27
+ }
28
+ resolve(rows);
29
+ });
30
+ parsedCSVStream.on('data', (parsedRow) => {
31
+ // a throw here escapes as an uncaught exception and leaves this
32
+ // promise forever unsettled, so it has to become a rejection
33
+ try {
34
+ const row = {};
35
+ for (const [header, value] of Object.entries(parsedRow)) {
36
+ // papaparse gathers the overflow from a row carrying more
37
+ // fields than there are headers under __parsed_extra, as an
38
+ // array rather than a cell any mapping can name
39
+ if (typeof value !== 'string') {
40
+ continue;
41
+ }
42
+ // hasOwn, not `in`: a prototype-named header would otherwise match an
43
+ // inherited method on the mapping and take a function as its mapped name.
44
+ if (headerMapping && Object.hasOwn(headerMapping, header)) {
45
+ row[headerMapping[header]] = value;
46
+ }
47
+ else if (isSafeColumnName(header)) {
48
+ // Carry any unmapped column through untouched, so the import is not
49
+ // constrained to a known vocabulary: a custom_fields.* column
50
+ // survives parsing even though nothing consumes it yet.
51
+ row[header] = value;
52
+ }
53
+ }
54
+ if (!Object.keys(row).length) {
55
+ return;
56
+ }
57
+ rows.push(row);
58
+ }
59
+ catch (err) {
60
+ reject(err);
61
+ }
62
+ });
63
+ });
64
+ }
@@ -0,0 +1,69 @@
1
+ import {pipeline} from 'node:stream';
2
+ import papaparse from 'papaparse';
3
+ import fs from 'fs-extra';
4
+
5
+ // A parsed CSV row: raw string cells, keyed by (renamed) column. Parsing is mechanical;
6
+ // giving the columns meaning and coercing their values is the domain's job (the import
7
+ // schema).
8
+ export type Row = Record<string, string>;
9
+
10
+ // A column named after an Object.prototype member (toString, __proto__, ...) is unsafe
11
+ // as a key: writing it would shadow a method or reach the prototype.
12
+ function isSafeColumnName(name: string): boolean {
13
+ return !(name in Object.prototype);
14
+ }
15
+
16
+ // headerMapping renames the CSV's headers to the columns they emit under; unmapped
17
+ // columns carry through under their own name.
18
+ export default function parse(path: string, headerMapping?: Record<string, string>): Promise<Row[]> {
19
+ return new Promise(function (resolve, reject) {
20
+ const csvFileStream = fs.createReadStream(path);
21
+ const csvParserStream = papaparse.parse(papaparse.NODE_STREAM_INPUT, {
22
+ header: true
23
+ });
24
+
25
+ const rows: Row[] = [];
26
+ const parsedCSVStream = pipeline(csvFileStream, csvParserStream, (err) => {
27
+ if (err) {
28
+ return reject(err);
29
+ }
30
+ resolve(rows);
31
+ });
32
+
33
+ parsedCSVStream.on('data', (parsedRow: Record<string, unknown>) => {
34
+ // a throw here escapes as an uncaught exception and leaves this
35
+ // promise forever unsettled, so it has to become a rejection
36
+ try {
37
+ const row: Row = {};
38
+
39
+ for (const [header, value] of Object.entries(parsedRow)) {
40
+ // papaparse gathers the overflow from a row carrying more
41
+ // fields than there are headers under __parsed_extra, as an
42
+ // array rather than a cell any mapping can name
43
+ if (typeof value !== 'string') {
44
+ continue;
45
+ }
46
+
47
+ // hasOwn, not `in`: a prototype-named header would otherwise match an
48
+ // inherited method on the mapping and take a function as its mapped name.
49
+ if (headerMapping && Object.hasOwn(headerMapping, header)) {
50
+ row[headerMapping[header]] = value;
51
+ } else if (isSafeColumnName(header)) {
52
+ // Carry any unmapped column through untouched, so the import is not
53
+ // constrained to a known vocabulary: a custom_fields.* column
54
+ // survives parsing even though nothing consumes it yet.
55
+ row[header] = value;
56
+ }
57
+ }
58
+
59
+ if (!Object.keys(row).length) {
60
+ return;
61
+ }
62
+
63
+ rows.push(row);
64
+ } catch (err) {
65
+ reject(err);
66
+ }
67
+ });
68
+ });
69
+ }
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = serialize;
7
+ const papaparse_1 = __importDefault(require("papaparse"));
8
+ // The papaparse boundary: given rows already shaped into cells, it writes CSV and
9
+ // nothing else. escapeFormulae is forced on -- papaparse defaults it off, but this CSV
10
+ // is opened by a person, so a leading =, +, -, @ or tab must be escaped.
11
+ function serialize(rows, { columns, header = true } = {}) {
12
+ return papaparse_1.default.unparse(rows, { columns, header, escapeFormulae: true });
13
+ }
@@ -0,0 +1,18 @@
1
+ import papaparse from 'papaparse';
2
+
3
+ interface SerializeOptions {
4
+ // Omitted, papaparse derives the columns (and their order) from the first row's
5
+ // keys -- so a typed shaper's row can define them, with no separate list to drift.
6
+ columns?: string[];
7
+ header?: boolean;
8
+ }
9
+
10
+ // The papaparse boundary: given rows already shaped into cells, it writes CSV and
11
+ // nothing else. escapeFormulae is forced on -- papaparse defaults it off, but this CSV
12
+ // is opened by a person, so a leading =, +, -, @ or tab must be escaped.
13
+ export default function serialize(
14
+ rows: Array<Record<string, unknown>>,
15
+ {columns, header = true}: SerializeOptions = {}
16
+ ): string {
17
+ return papaparse.unparse(rows, {columns, header, escapeFormulae: true});
18
+ }
@@ -0,0 +1,202 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.toExportCsvRow = toExportCsvRow;
7
+ const moment_1 = __importDefault(require("moment"));
8
+ const node_stream_1 = require("node:stream");
9
+ const logging = require('@tryghost/logging');
10
+ const errors = require('@tryghost/errors');
11
+ const { csvCellsForFields } = require('@tryghost/custom-field-types/csv');
12
+ // The columns the export reads off the members table, checked against MemberDbRow so the
13
+ // query cannot select a column the row type does not declare.
14
+ const MEMBER_COLUMNS = ['id', 'email', 'name', 'note', 'status', 'created_at'];
15
+ function namesToCsv(items) {
16
+ return items.map(item => item.name).join(',');
17
+ }
18
+ // Encode a flattened export row into its CSV cells, type-checked against MemberExportRow:
19
+ // a change to that row must be handled here or this stops compiling. deleted_at is always
20
+ // null -- the export reads live members -- and complimentary_plan is written from comped.
21
+ function toExportCsvRow(row) {
22
+ return {
23
+ id: row.id,
24
+ email: row.email,
25
+ name: row.name,
26
+ note: row.note,
27
+ subscribed_to_emails: row.subscribed === true ? 'true' : 'false',
28
+ complimentary_plan: row.comped === true ? 'true' : '',
29
+ stripe_customer_id: row.stripe_customer_id,
30
+ created_at: row.created_at,
31
+ deleted_at: null,
32
+ labels: namesToCsv(row.labels),
33
+ tiers: namesToCsv(row.tiers),
34
+ gift_id: row.gift_id || '',
35
+ ...row.custom_field_cells
36
+ };
37
+ }
38
+ const BATCH_SIZE = 1000;
39
+ // Streams the matching members as flat rows, each carrying its related data (tiers,
40
+ // labels, subscription, gift, Stripe customer, custom fields) for the output serializer
41
+ // to turn into CSV lines. The members aggregate and custom fields are reached only
42
+ // through the ports above, so nothing Bookshelf- or flag-shaped leaks into the export.
43
+ class MembersCSVExporter {
44
+ _knex;
45
+ _members;
46
+ _customFields;
47
+ constructor({ knex, members, customFields }) {
48
+ this._knex = knex;
49
+ this._members = members;
50
+ this._customFields = customFields;
51
+ }
52
+ async export(options = {}) {
53
+ const start = Date.now();
54
+ const hasFilter = options.limit !== 'all' || options.filter || options.search;
55
+ const ids = hasFilter ? await this._members.findFilteredIds(options) : null;
56
+ if (ids) {
57
+ logging.info(`[MembersExporter] Found ${ids.length} members matching filter criteria`);
58
+ }
59
+ const reference = await this.fetchReferenceData();
60
+ const membersQuery = this._knex('members').select(...MEMBER_COLUMNS);
61
+ if (ids) {
62
+ membersQuery.whereIn('id', ids);
63
+ }
64
+ logging.info('[MembersExporter] Starting streaming export of members');
65
+ const batchingTransform = this.createBatchingTransform();
66
+ const processingTransform = this.createProcessingTransform(reference);
67
+ (0, node_stream_1.pipeline)(membersQuery.stream(), batchingTransform, processingTransform, (err) => {
68
+ if (err) {
69
+ logging.error({ event: { name: 'members-export.stream.error' }, err }, 'Members export stream failed');
70
+ }
71
+ else {
72
+ logging.info('[MembersExporter] Total time taken for member export: ' + (Date.now() - start) / 1000 + 's');
73
+ }
74
+ });
75
+ return processingTransform;
76
+ }
77
+ // products and labels are small, stable tables, read once up front as id->name
78
+ // maps. Custom field definitions are read once too, which fixes the column set for
79
+ // the whole file: archiving a field mid-export cannot leave the header ragged. It
80
+ // can still leave that field's cells empty from the batch after the archive
81
+ // onwards, because the per-batch value read applies its own active filter.
82
+ async fetchReferenceData() {
83
+ const start = Date.now();
84
+ const allProducts = await this._knex('products').select('id', 'name').then(rows => rows.reduce((acc, product) => {
85
+ acc[product.id] = product.name;
86
+ return acc;
87
+ }, {}));
88
+ const allLabels = await this._knex('labels').select('id', 'name').then(rows => rows.reduce((acc, label) => {
89
+ acc[label.id] = label.name;
90
+ return acc;
91
+ }, {}));
92
+ logging.info('[MembersExporter] Fetched products and labels in ' + (Date.now() - start) + 'ms');
93
+ const activeCustomFields = await this._customFields.activeDefinitions();
94
+ return { allProducts, allLabels, activeCustomFields };
95
+ }
96
+ // Group the member stream into batches so the related-data reads are one query per
97
+ // batch rather than one per member.
98
+ createBatchingTransform() {
99
+ let currentBatch = [];
100
+ return new node_stream_1.Transform({
101
+ objectMode: true,
102
+ transform(member, encoding, callback) {
103
+ currentBatch.push(member);
104
+ if (currentBatch.length >= BATCH_SIZE) {
105
+ this.push(currentBatch);
106
+ currentBatch = [];
107
+ }
108
+ callback();
109
+ },
110
+ flush(callback) {
111
+ if (currentBatch.length > 0) {
112
+ this.push(currentBatch);
113
+ }
114
+ callback();
115
+ }
116
+ });
117
+ }
118
+ // Each flattened row is pushed individually so a batch's rows do not pile up in memory.
119
+ createProcessingTransform(reference) {
120
+ const assembleRelatedData = (memberIds) => this.assembleRelatedData(memberIds, reference.activeCustomFields);
121
+ const flattenBatch = (members, related) => this.flattenBatch(members, related, reference);
122
+ return new node_stream_1.Transform({
123
+ objectMode: true,
124
+ highWaterMark: BATCH_SIZE,
125
+ async transform(batch, encoding, callback) {
126
+ try {
127
+ const memberIds = batch.map(member => member.id);
128
+ const related = await assembleRelatedData(memberIds);
129
+ const rows = flattenBatch(batch, related);
130
+ rows.forEach(row => this.push(row));
131
+ callback();
132
+ }
133
+ catch (err) {
134
+ callback(err instanceof Error ? err : new errors.InternalServerError({ message: String(err) }));
135
+ }
136
+ }
137
+ });
138
+ }
139
+ // One query per related table for the whole batch, not one per member. Custom field
140
+ // values are read only when there are columns to fill.
141
+ async assembleRelatedData(memberIds, activeCustomFields) {
142
+ const knex = this._knex;
143
+ const [tiers, labels, stripeCustomers, subscriptions, gifts, customFieldValuesMap] = await Promise.all([
144
+ knex('members_products')
145
+ .select('member_id', knex.raw('GROUP_CONCAT(product_id) as tiers'))
146
+ .whereIn('member_id', memberIds)
147
+ .groupBy('member_id'),
148
+ knex('members_labels')
149
+ .select('member_id', knex.raw('GROUP_CONCAT(label_id) as labels'))
150
+ .whereIn('member_id', memberIds)
151
+ .groupBy('member_id'),
152
+ knex('members_stripe_customers')
153
+ .select('member_id', knex.raw('MIN(customer_id) as stripe_customer_id'))
154
+ .whereIn('member_id', memberIds)
155
+ .groupBy('member_id'),
156
+ knex('members_newsletters')
157
+ .distinct('member_id')
158
+ .whereIn('member_id', memberIds),
159
+ knex('gifts')
160
+ .select('id', 'redeemer_member_id')
161
+ .where('status', 'redeemed')
162
+ .whereIn('redeemer_member_id', memberIds),
163
+ activeCustomFields.length > 0
164
+ ? this._customFields.valuesForMembers(memberIds)
165
+ : new Map()
166
+ ]);
167
+ return {
168
+ tiersMap: new Map(tiers.map((row) => [row.member_id, row.tiers])),
169
+ labelsMap: new Map(labels.map((row) => [row.member_id, row.labels])),
170
+ stripeCustomerMap: new Map(stripeCustomers.map((row) => [row.member_id, row.stripe_customer_id])),
171
+ subscribedSet: new Set(subscriptions.map((row) => row.member_id)),
172
+ giftIdMap: new Map(gifts.map((row) => [row.redeemer_member_id, row.id])),
173
+ customFieldValuesMap
174
+ };
175
+ }
176
+ flattenBatch(members, related, reference) {
177
+ const { tiersMap, labelsMap, stripeCustomerMap, subscribedSet, giftIdMap, customFieldValuesMap } = related;
178
+ const { allProducts, allLabels, activeCustomFields } = reference;
179
+ return members.map((row) => {
180
+ const tierConcat = tiersMap.get(row.id);
181
+ const tierIds = tierConcat ? tierConcat.split(',') : [];
182
+ const labelConcat = labelsMap.get(row.id);
183
+ const labelIds = labelConcat ? labelConcat.split(',') : [];
184
+ return {
185
+ ...row,
186
+ subscribed: subscribedSet.has(row.id),
187
+ comped: row.status === 'comped',
188
+ gift_id: giftIdMap.get(row.id) || null,
189
+ stripe_customer_id: stripeCustomerMap.get(row.id) || null,
190
+ created_at: (0, moment_1.default)(row.created_at).toISOString(),
191
+ tiers: tierIds.map(id => ({ name: allProducts[id] })),
192
+ labels: labelIds.map(id => ({ name: allLabels[id] })),
193
+ // Flattened here rather than in the serializer because the column set is
194
+ // only knowable from the database. Every member carries a cell for every
195
+ // active field's column, so a member with no values still contributes the
196
+ // full set -- the CSV header is taken from whichever row streams first.
197
+ custom_field_cells: csvCellsForFields(activeCustomFields, customFieldValuesMap.get(row.id) || {})
198
+ };
199
+ });
200
+ }
201
+ }
202
+ exports.default = MembersCSVExporter;