ghost 6.54.0 → 6.55.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 (327) 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-custom-field-types-0.0.0.tgz +0 -0
  6. package/components/tryghost-kg-default-cards-10.3.3.tgz +0 -0
  7. package/core/boot.js +3 -6
  8. package/core/built/admin/assets/{account-migration-DLvVA5yq.js → account-migration-sjHLx5dy.js} +1 -1
  9. package/core/built/admin/assets/activitypub/{account-migration-DHbZcxGM.js → account-migration-DoYXtnZ8.js} +4 -4
  10. package/core/built/admin/assets/activitypub/activitypub.js +2 -2
  11. package/core/built/admin/assets/activitypub/{bluesky-sharing-zfxUey_L.js → bluesky-sharing-Bxeh6MKW.js} +4 -4
  12. package/core/built/admin/assets/activitypub/{dist-BqkDUQxJ2.js → dist-ByFmDSNP2.js} +64 -63
  13. package/core/built/admin/assets/activitypub/{dist-Ba_QxHHl.js → dist-D8sFfdpC.js} +2 -2
  14. package/core/built/admin/assets/activitypub/{domain-CmwSZLHK.js → domain-CWueFW6q.js} +5 -5
  15. package/core/built/admin/assets/activitypub/{edit-profile-CXdwNmVG.js → edit-profile-BGxPU7N5.js} +3 -3
  16. package/core/built/admin/assets/activitypub/{explore-DfbjzGM1.js → explore-DBK-u0Ch.js} +3 -3
  17. package/core/built/admin/assets/activitypub/{feed-DSEBJOqf.js → feed-Bcir5RQZ.js} +3 -3
  18. package/core/built/admin/assets/activitypub/{field-CY7XVcx6.js → field-BV-CIT3d.js} +2 -2
  19. package/core/built/admin/assets/activitypub/{inbox-DtQ0kytT.js → inbox-yC0nfvzl.js} +4 -4
  20. package/core/built/admin/assets/activitypub/{label-Dw96GQ7u.js → label-DTRS9TDk.js} +2 -2
  21. package/core/built/admin/assets/activitypub/layout-qx-bUL1k.js +1 -0
  22. package/core/built/admin/assets/activitypub/{moderation-Dg_-I-8C.js → moderation-Dwl9t0pb.js} +4 -4
  23. package/core/built/admin/assets/activitypub/{note-Cy1-uKMY.js → note-B-zKsZ7r.js} +3 -3
  24. package/core/built/admin/assets/activitypub/{notifications-B_fvoLIM.js → notifications-DjJZD54t.js} +3 -3
  25. package/core/built/admin/assets/activitypub/{preferences-ByJseZp6.js → preferences-CoU8vs6d.js} +6 -6
  26. package/core/built/admin/assets/activitypub/{profile-BFtbnIjz.js → profile-BAsaJekx.js} +6 -6
  27. package/core/built/admin/assets/activitypub/{routes-Dh3xXfce.js → routes-CLH3OVTP.js} +28 -25
  28. package/core/built/admin/assets/activitypub/{settings-DTgqJPxM.js → settings-DA6pNJ-Q.js} +3 -3
  29. package/core/built/admin/assets/activitypub/{site-CdL9ZLqm.js → site-CQD42Pw5.js} +2 -2
  30. package/core/built/admin/assets/activitypub/{tabs-ogqwoBlb.js → tabs-Cg0saow1.js} +7 -7
  31. package/core/built/admin/assets/activitypub/{use-reply-chain-data-CxonteFD.js → use-reply-chain-data-Br4aOWOh.js} +2 -2
  32. package/core/built/admin/assets/audience-DjEWNhp3.js +1 -0
  33. package/core/built/admin/assets/{automated-emails-Cn87L5Md.js → automated-emails-DRy7qY-J.js} +2 -2
  34. package/core/built/admin/assets/{automations-BP_aHamJ.js → automations-Bx3VcFIL.js} +1 -1
  35. package/core/built/admin/assets/{automations-B7-y3dXt.js → automations-DhKeS_U5.js} +1 -1
  36. package/core/built/admin/assets/{bluesky-sharing-Bh36XPh4.js → bluesky-sharing-ClMd0-Ft.js} +1 -1
  37. package/core/built/admin/assets/{breadcrumb-D34YWnaw.js → breadcrumb-BD0U2ygg.js} +1 -1
  38. package/core/built/admin/assets/{chart-helpers-DbklaJw9.js → chart-helpers-fjQKRVyY.js} +1 -1
  39. package/core/built/admin/assets/checkbox-CWa28lgO.js +1 -0
  40. package/core/built/admin/assets/{chunk.962.b73442b07be930bb5d85.js → chunk.211.6f7328d63c41536451b3.js} +737 -559
  41. package/core/built/admin/assets/{chunk.962.b73442b07be930bb5d85.js.LICENSE.txt → chunk.211.6f7328d63c41536451b3.js.LICENSE.txt} +2 -0
  42. package/core/built/admin/assets/{chunk.589.096f4d0bb775e5229aa9.js → chunk.481.6c7395a49484bb4583aa.js} +41 -38
  43. package/core/built/admin/assets/chunk.524.cffd399783a519e404a9.js +25 -0
  44. package/core/built/admin/assets/chunk.582.eebf0d0b2d202bb746a5.js +27 -0
  45. package/core/built/admin/assets/code-editor-view-CCr0rBWS.js +1 -0
  46. package/core/built/admin/assets/comments-DyUSMUdj.js +1 -0
  47. package/core/built/admin/assets/{date-range-select-B5vSsrpx.js → date-range-select-BFp58EMk.js} +1 -1
  48. package/core/built/admin/assets/{disabled-sources-indicator-NkOIoMue.js → disabled-sources-indicator-CUxMfiPa.js} +1 -1
  49. package/core/built/admin/assets/dist-2Tnz2Yfq.js +1 -0
  50. package/core/built/admin/assets/dist-CjY-f5Z1.js +1 -0
  51. package/core/built/admin/assets/dist-DIC8yz5C.js +1 -0
  52. package/core/built/admin/assets/domain-C0bV2UV8.js +1 -0
  53. package/core/built/admin/assets/dropzone-DlK-K2DQ.js +5 -0
  54. package/core/built/admin/assets/{edit-profile-rvDGrS8J.js → edit-profile-BKT-REJw.js} +1 -1
  55. package/core/built/admin/assets/editor-Dp865O-4.js +8 -0
  56. package/core/built/admin/assets/{explore-BZAZOg5X.js → explore-ClUpJZOp.js} +1 -1
  57. package/core/built/admin/assets/{feed-CFV8xDw9.js → feed-DF92dUMP.js} +1 -1
  58. package/core/built/admin/assets/field-Do7lieBA.js +1 -0
  59. package/core/built/admin/assets/{filter-sources-Fx8wiYYw.js → filter-sources-BV2rnpfA.js} +1 -1
  60. package/core/built/admin/assets/filters-CUXJ2Nm9.js +1 -0
  61. package/core/built/admin/assets/{filters-CGX_pf2N.js → filters-DfbIWc_l.js} +1 -1
  62. package/core/built/admin/assets/{ghost-3063eae29e6c9cc6d6e7131928a27da0.css → ghost-a9c788f6f0b1ea6cf0f14ca4b13f4496.css} +1 -1
  63. package/core/built/admin/assets/{ghost-b1dce91ad3232cc259ee5fbbf9526243.js → ghost-c4057474943effc568c5659ccc4909d7.js} +407 -406
  64. package/core/built/admin/assets/{ghost-dark-c8d0ab7f515b2010c173e23e9b32ea8a.css → ghost-dark-db36d795d21c61426837df40af270966.css} +1 -1
  65. package/core/built/admin/assets/gift-link-modal-f9VIMNDv.js +1 -0
  66. package/core/built/admin/assets/{gift-links-nBxJjZiQ.js → gift-links-CsUhefWG.js} +1 -1
  67. package/core/built/admin/assets/growth-B3I6gNLm.js +1 -0
  68. package/core/built/admin/assets/{Growth-CSaHoSVD.js → growth-CVw1My4S.js} +2 -2
  69. package/core/built/admin/assets/{hooks-BApwwBE0.js → hooks-CixO01lL.js} +1 -1
  70. package/core/built/admin/assets/{inbox-BRp1_aaC.js → inbox-CFBSu4Ed.js} +12 -12
  71. package/core/built/admin/assets/index-9Cw8z3rA.css +1 -0
  72. package/core/built/admin/assets/index-BGG7NckO.js +67 -0
  73. package/core/built/admin/assets/input-group-BSgG_lVw.js +1 -0
  74. package/core/built/admin/assets/input-surface-BpAjZXY5.js +1 -0
  75. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +974 -937
  76. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +28 -28
  77. package/core/built/admin/assets/koenig-lexical/style.css +1 -1
  78. package/core/built/admin/assets/{koenig-lexical-CM7UFNdL.js → koenig-lexical-f8vHRYvz.js} +28 -28
  79. package/core/built/admin/assets/{kpi-card-BrGH6FsT.js → kpi-card-DSap9rAy.js} +1 -1
  80. package/core/built/admin/assets/{kpi-card-CIGyijjx.js → kpi-card-DU0rn9Cj.js} +1 -1
  81. package/core/built/admin/assets/{kpi-helpers-_HRRsklE.js → kpi-helpers-Dz33AV6h.js} +1 -1
  82. package/core/built/admin/assets/{kpi-tabs-BSvh9eoq.js → kpi-tabs-BtTTrKvJ.js} +1 -1
  83. package/core/built/admin/assets/{label-BOR9XdKl.js → label-DS8Mi6G5.js} +1 -1
  84. package/core/built/admin/assets/label-picker-1DvSuLA1.js +1 -0
  85. package/core/built/admin/assets/labels-CDSUt4J_.js +1 -0
  86. package/core/built/admin/assets/layout-yQEi2qDM.js +1 -0
  87. package/core/built/admin/assets/{link-helpers-tOlxmuH5.js → link-helpers-7EYqvvHs.js} +1 -1
  88. package/core/built/admin/assets/member-custom-fields-yOMHyAg9.js +1 -0
  89. package/core/built/admin/assets/member-detail-BAq7gDaa.js +1 -0
  90. package/core/built/admin/assets/members-CQpVFJu4.js +10 -0
  91. package/core/built/admin/assets/modals-ReORvWnu.js +31 -0
  92. package/core/built/admin/assets/moderation-DySbgyUS.js +1 -0
  93. package/core/built/admin/assets/newsletter-CJQvtHqN.js +1 -0
  94. package/core/built/admin/assets/{Newsletters-C5cs6FGo.js → newsletters-B3QQXfGT.js} +1 -1
  95. package/core/built/admin/assets/{newsletters-D1jbtV-h.js → newsletters-BXJbJgA-.js} +1 -1
  96. package/core/built/admin/assets/note-C8kxrz6w.js +1 -0
  97. package/core/built/admin/assets/notifications-CgtSfX7t.js +1 -0
  98. package/core/built/admin/assets/{offers-CrY6nSRR.js → offers-DEniVRD5.js} +1 -1
  99. package/core/built/admin/assets/onboarding-route-4qtziA9d.js +1 -0
  100. package/core/built/admin/assets/overview-Bf2sbznq.js +1 -0
  101. package/core/built/admin/assets/overview-CBfePxcr.js +1 -0
  102. package/core/built/admin/assets/{page-header-C0jVxtS8.js → page-header-B3Wb9Jqv.js} +1 -1
  103. package/core/built/admin/assets/{pages-BbNWFcGj.js → pages-B4goRPUn.js} +1 -1
  104. package/core/built/admin/assets/{post-analytics-Iw7dk1mT.js → post-analytics-We-ko5Cb.js} +1 -1
  105. package/core/built/admin/assets/post-analytics-header-Bmc9su6q.js +1 -0
  106. package/core/built/admin/assets/post-analytics-provider-B6UynTGb.js +1 -0
  107. package/core/built/admin/assets/{post-share-modal-DBfKl2GZ.js → post-share-modal-DFfO93_0.js} +1 -1
  108. package/core/built/admin/assets/{posts-EWpZIjkr.js → posts-DhXE7b6r.js} +1 -1
  109. package/core/built/admin/assets/{preferences-D4EANGoc.js → preferences-DJPfHEGu.js} +2 -2
  110. package/core/built/admin/assets/profile-DqBQp-gT.js +1 -0
  111. package/core/built/admin/assets/{referrers-BCfFYSHc.js → referrers-pmJT4k3-.js} +1 -1
  112. package/core/built/admin/assets/select-C5_xEt5b.js +1 -0
  113. package/core/built/admin/assets/settings-BwiVIXau.js +4 -0
  114. package/core/built/admin/assets/{settings-Bz3RZRmE.js → settings-WXNeWyU1.js} +1 -1
  115. package/core/built/admin/assets/shade-app-B3heOAfz-CGXQwWeO.js +3 -0
  116. package/core/built/admin/assets/{share-modal-CkdTTMJd.js → share-modal-DsgW3wbg.js} +1 -1
  117. package/core/built/admin/assets/social-urls-BQPFEk1_.js +8 -0
  118. package/core/built/admin/assets/{sort-button-DZp9Y2r1.js → sort-button-Bb_FCl7e.js} +1 -1
  119. package/core/built/admin/assets/{source-icon-CHr9qJ2Y.js → source-icon-BeaRxUn9.js} +1 -1
  120. package/core/built/admin/assets/{stats-DGoXZxWE.js → stats-Bjj_kmJ1.js} +1 -1
  121. package/core/built/admin/assets/{stats-view-D2-1L0ms.js → stats-view-BxPbW6K1.js} +1 -1
  122. package/core/built/admin/assets/{step-1-RdahbwyG.js → step-1-B1e3-1F8.js} +1 -1
  123. package/core/built/admin/assets/{step-2-D83PeRwd.js → step-2-C5WaeBJo.js} +1 -1
  124. package/core/built/admin/assets/{step-3-p_Op4wv4.js → step-3-B46iVPwu.js} +2 -2
  125. package/core/built/admin/assets/switch-z5Y6Uzt6.js +1 -0
  126. package/core/built/admin/assets/{table-B8LgTQJZ.js → table-CB-4_HBJ.js} +1 -1
  127. package/core/built/admin/assets/tabs-CCEgKYzq.js +1 -0
  128. package/core/built/admin/assets/{tags-B4l891z2.js → tags-Cg2VJGmG.js} +1 -1
  129. package/core/built/admin/assets/textarea-Bjccvh_E.js +1 -0
  130. package/core/built/admin/assets/{tiers-BgYLWRJv.js → tiers-DYsa4DCo.js} +1 -1
  131. package/core/built/admin/assets/{toggle-group-DNXDkAwf.js → toggle-group-b3IlxvhV.js} +1 -1
  132. package/core/built/admin/assets/{topic-filter-DZGjEcz3.js → topic-filter-DAFT87tQ.js} +1 -1
  133. package/core/built/admin/assets/{use-analytics-data-CaT8RirX.js → use-analytics-data-S0FTd43s.js} +1 -1
  134. package/core/built/admin/assets/use-filter-params-Gpoq2E1L.js +1 -0
  135. package/core/built/admin/assets/{use-growth-stats-9Db2CuJe.js → use-growth-stats-CIaiHOVb.js} +1 -1
  136. package/core/built/admin/assets/{use-post-referrers-BV54YcQS.js → use-post-referrers-Cq2vBPrA.js} +1 -1
  137. package/core/built/admin/assets/{use-reply-chain-data-DuYZGDan.js → use-reply-chain-data-DBDRtGu-.js} +1 -1
  138. package/core/built/admin/assets/{use-simple-pagination-8fTua9Nt.js → use-simple-pagination-CjYNxdwe.js} +1 -1
  139. package/core/built/admin/assets/{use-tinybird-query-Dz156HvZ.js → use-tinybird-query-falHAtuK.js} +1 -1
  140. package/core/built/admin/assets/{use-tinybird-token-C9RUdL_n.js → use-tinybird-token-D7-8ND1y.js} +1 -1
  141. package/core/built/admin/assets/{users-CcSCQFcQ.js → users-Dkv8NQZe.js} +1 -1
  142. package/core/built/admin/assets/{virtual-list-BCvP0YF-.js → virtual-list-CvnC9zF2.js} +1 -1
  143. package/core/built/admin/assets/web-B_5GZRvH.js +1 -0
  144. package/core/built/admin/assets/web-jFYWCKDn.js +2 -0
  145. package/core/built/admin/index.html +14 -10
  146. package/core/frontend/services/proxy.js +10 -1
  147. package/core/frontend/services/routing/router-manager.js +14 -14
  148. package/core/frontend/services/routing/static-routes-router.js +8 -10
  149. package/core/server/adapters/storage/LocalFilesStorage.js +13 -11
  150. package/core/server/adapters/storage/LocalFilesStorage.ts +24 -0
  151. package/core/server/adapters/storage/LocalImagesStorage.js +14 -12
  152. package/core/server/adapters/storage/LocalImagesStorage.ts +24 -0
  153. package/core/server/adapters/storage/LocalMediaStorage.js +13 -11
  154. package/core/server/adapters/storage/LocalMediaStorage.ts +24 -0
  155. package/core/server/adapters/storage/LocalStorageBase.js +107 -167
  156. package/core/server/adapters/storage/LocalStorageBase.ts +342 -0
  157. package/core/server/adapters/storage/utils.js +36 -50
  158. package/core/server/adapters/storage/utils.ts +60 -0
  159. package/core/server/api/endpoints/automation-action-links.js +68 -0
  160. package/core/server/api/endpoints/automation-action-links.ts +46 -0
  161. package/core/server/api/endpoints/db.js +6 -0
  162. package/core/server/api/endpoints/index.js +4 -0
  163. package/core/server/api/endpoints/members.js +25 -28
  164. package/core/server/api/endpoints/pages.js +6 -1
  165. package/core/server/api/endpoints/posts.js +11 -2
  166. package/core/server/api/endpoints/settings.js +0 -2
  167. package/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +4 -1
  168. package/core/server/api/endpoints/utils/serializers/output/members.js +18 -87
  169. package/core/server/api/endpoints/utils/serializers/output/utils/url.js +11 -1
  170. package/core/server/data/exporter/export-filename.js +2 -1
  171. package/core/server/data/importer/importers/data/revue-subscriber-importer.js +6 -10
  172. package/core/server/data/importer/importers/json-to-html.js +24 -4
  173. package/core/server/data/migrations/versions/6.55/2026-07-27-11-25-44-add-to-hash-to-redirects.js +51 -0
  174. package/core/server/data/migrations/versions/6.55/2026-07-28-12-08-00-remove-routes-hash-from-settings.js +5 -0
  175. package/core/server/data/schema/commands.js +5 -0
  176. package/core/server/data/schema/default-settings/default-settings.json +0 -4
  177. package/core/server/data/schema/schema.js +5 -1
  178. package/core/server/lib/image/image-size.js +3 -17
  179. package/core/server/lib/image/image-utils.js +15 -1
  180. package/core/server/lib/request-external.js +1 -1
  181. package/core/server/models/user.js +46 -70
  182. package/core/server/services/auth/session/session-service.js +28 -1
  183. package/core/server/services/automations/automations-api.js +21 -6
  184. package/core/server/services/automations/automations-api.ts +25 -6
  185. package/core/server/services/automations/automations-repository.ts +19 -0
  186. package/core/server/services/automations/database-automations-repository.js +116 -28
  187. package/core/server/services/automations/database-automations-repository.ts +118 -9
  188. package/core/server/services/automations/database-date.js +10 -0
  189. package/core/server/services/automations/database-date.ts +5 -0
  190. package/core/server/services/automations/poll.js +5 -2
  191. package/core/server/services/automations/poll.ts +7 -2
  192. package/core/server/services/automations/scheduler-poll-time.js +40 -0
  193. package/core/server/services/automations/scheduler-poll-time.ts +38 -0
  194. package/core/server/services/automations/service.js +15 -3
  195. package/core/server/services/automations/service.ts +16 -3
  196. package/core/server/services/comments/comments-service-emails.js +14 -8
  197. package/core/server/services/comments/comments-service.js +9 -4
  198. package/core/server/services/email-rendering/partials/email-wrapper.hbs +3 -1
  199. package/core/server/services/email-service/email-service-wrapper.js +6 -1
  200. package/core/server/services/email-service/email-templates/partials/styles.hbs +8 -0
  201. package/core/server/services/email-service/newsletter-email-event-storage.js +41 -10
  202. package/core/server/services/lib/dynamic-redirect-manager.js +12 -3
  203. package/core/server/services/link-redirection/link-redirect-repository.js +41 -4
  204. package/core/server/services/link-redirection/link-redirect.js +2 -0
  205. package/core/server/services/link-redirection/link-redirect.ts +3 -1
  206. package/core/server/services/link-redirection/link-redirects-service.js +38 -2
  207. package/core/server/services/link-tracking/click-event.js +3 -0
  208. package/core/server/services/link-tracking/index.js +38 -6
  209. package/core/server/services/link-tracking/link-click-repository.js +30 -9
  210. package/core/server/services/link-tracking/link-click-tracking-service.js +44 -3
  211. package/core/server/services/member-attribution/outbound-link-tagger.js +3 -1
  212. package/core/server/services/member-welcome-emails/member-welcome-email-renderer.js +19 -4
  213. package/core/server/services/member-welcome-emails/service.js +11 -3
  214. package/core/server/services/members/{importer → import-export}/config.js +4 -9
  215. package/core/server/services/members/{importer → import-export}/config.ts +4 -9
  216. package/core/server/services/members/import-export/csv/formula.js +16 -0
  217. package/core/server/services/members/import-export/csv/formula.ts +14 -0
  218. package/core/server/services/members/import-export/csv/index.js +12 -0
  219. package/core/server/services/members/import-export/csv/index.ts +3 -0
  220. package/core/server/services/members/import-export/csv/parse.js +64 -0
  221. package/core/server/services/members/import-export/csv/parse.ts +69 -0
  222. package/core/server/services/members/import-export/csv/serialize.js +13 -0
  223. package/core/server/services/members/import-export/csv/serialize.ts +18 -0
  224. package/core/server/services/members/import-export/export/exporter.js +202 -0
  225. package/core/server/services/members/import-export/export/exporter.ts +325 -0
  226. package/core/server/services/members/import-export/import/completion-email.js +84 -0
  227. package/core/server/services/members/import-export/import/completion-email.ts +141 -0
  228. package/core/server/services/members/import-export/import/importer.js +297 -0
  229. package/core/server/services/members/import-export/import/importer.ts +468 -0
  230. package/core/server/services/members/import-export/import/reader.js +32 -0
  231. package/core/server/services/members/import-export/import/reader.ts +33 -0
  232. package/core/server/services/members/import-export/import/row.js +33 -0
  233. package/core/server/services/members/import-export/import/row.ts +52 -0
  234. package/core/server/services/members/import-export/import/spool.js +30 -0
  235. package/core/server/services/members/import-export/import/spool.ts +38 -0
  236. package/core/server/services/members/import-export/index.js +99 -0
  237. package/core/server/services/members/import-export/index.ts +137 -0
  238. package/core/server/services/members/members-api/controllers/router-controller.js +7 -0
  239. package/core/server/services/members/service.js +25 -20
  240. package/core/server/services/members-custom-fields/values-service.js +19 -7
  241. package/core/server/services/members-custom-fields/values-service.ts +19 -7
  242. package/core/server/services/mentions/service.js +5 -1
  243. package/core/server/services/posts/posts-exporter.js +6 -4
  244. package/core/server/services/posts/posts-service.js +2 -2
  245. package/core/server/services/route-settings/activation-bridge.js +59 -49
  246. package/core/server/services/route-settings/activation-bridge.ts +91 -51
  247. package/core/server/services/route-settings/dynamic-routing-service.js +22 -51
  248. package/core/server/services/route-settings/index.js +1 -16
  249. package/core/server/services/settings/settings-service.js +0 -18
  250. package/core/server/services/themes/storage.js +9 -0
  251. package/core/server/services/themes/theme-storage.js +1 -1
  252. package/core/server/services/url/url-service-facade.js +1 -1
  253. package/core/server/services/url/url-service-facade.ts +8 -1
  254. package/core/server/web/api/endpoints/admin/routes.js +1 -0
  255. package/core/shared/config/defaults.json +2 -1
  256. package/core/shared/express.js +6 -1
  257. package/core/shared/labs.js +2 -1
  258. package/package.json +11 -11
  259. package/pnpm-lock.yaml +18 -18
  260. package/pnpm-workspace.yaml +8 -7
  261. package/components/tryghost-adapter-base-scheduling-0.1.1.tgz +0 -0
  262. package/core/built/admin/assets/Overview-j4NaeUNm.js +0 -1
  263. package/core/built/admin/assets/Web-_fD935OX.js +0 -2
  264. package/core/built/admin/assets/activitypub/layout-IdVq3lRy.js +0 -1
  265. package/core/built/admin/assets/audience-BKRdsFmy.js +0 -1
  266. package/core/built/admin/assets/checkbox-DA4-1UiI.js +0 -1
  267. package/core/built/admin/assets/chunk.524.45e0875f41b71e46e849.js +0 -25
  268. package/core/built/admin/assets/chunk.582.51defd4b009f0de8d4d5.js +0 -27
  269. package/core/built/admin/assets/code-editor-view-BKBrkffZ.js +0 -1
  270. package/core/built/admin/assets/comments-CzVNIT8b.js +0 -1
  271. package/core/built/admin/assets/constants-c-JNF9c2.js +0 -1
  272. package/core/built/admin/assets/dist-CT46_f_J.js +0 -1
  273. package/core/built/admin/assets/domain-B7_n5nfx.js +0 -1
  274. package/core/built/admin/assets/dropzone-DcuJEEUf.js +0 -5
  275. package/core/built/admin/assets/editor-BCiIyFjm.js +0 -8
  276. package/core/built/admin/assets/field-BW-M2Zwp.js +0 -1
  277. package/core/built/admin/assets/filters-DtcXMZDU.js +0 -1
  278. package/core/built/admin/assets/gift-link-modal-Bk8qpnMw.js +0 -1
  279. package/core/built/admin/assets/growth-Ck2s3JjR.js +0 -1
  280. package/core/built/admin/assets/index-BBf502Wg.js +0 -69
  281. package/core/built/admin/assets/index-BCCZKxP2.css +0 -1
  282. package/core/built/admin/assets/input-group-DjuYnNCd.js +0 -1
  283. package/core/built/admin/assets/label-picker-BdNAWuUX.js +0 -1
  284. package/core/built/admin/assets/labels-Cyayfd-e.js +0 -1
  285. package/core/built/admin/assets/layout-Bw4GnxXp.js +0 -1
  286. package/core/built/admin/assets/member-custom-fields-DvFyamLu.js +0 -1
  287. package/core/built/admin/assets/member-detail-Cg2vTqvJ.js +0 -1
  288. package/core/built/admin/assets/members-DsPxYAVm.js +0 -10
  289. package/core/built/admin/assets/modals-Bf_fyvHZ.js +0 -31
  290. package/core/built/admin/assets/moderation-Dlp7M5sa.js +0 -1
  291. package/core/built/admin/assets/multi-select-combobox-CH1o8XXQ.js +0 -1
  292. package/core/built/admin/assets/newsletter-CQ5CAYUI.js +0 -1
  293. package/core/built/admin/assets/note-BrAZ7Z0o.js +0 -1
  294. package/core/built/admin/assets/notifications-BnbHLOPC.js +0 -1
  295. package/core/built/admin/assets/onboarding-route-eU146kuA.js +0 -1
  296. package/core/built/admin/assets/overview-BpbaK-xu.js +0 -1
  297. package/core/built/admin/assets/post-analytics-header-D9pJ4iNm.js +0 -1
  298. package/core/built/admin/assets/post-analytics-provider-BDqcfBor.js +0 -1
  299. package/core/built/admin/assets/profile-dJ44Bn_W.js +0 -1
  300. package/core/built/admin/assets/select--2uVN9_-.js +0 -1
  301. package/core/built/admin/assets/settings-SD9bxIpt.js +0 -4
  302. package/core/built/admin/assets/social-urls-BvFZwYcE.js +0 -8
  303. package/core/built/admin/assets/switch-qp518gFQ.js +0 -1
  304. package/core/built/admin/assets/tabs-D3ZOYkM7.js +0 -1
  305. package/core/built/admin/assets/textarea-CTlb3Uu6.js +0 -1
  306. package/core/built/admin/assets/use-filter-params-DeQ8sX4N.js +0 -1
  307. package/core/built/admin/assets/web-DTZnnUGr.js +0 -1
  308. package/core/server/services/mail/templates/newsletter.html +0 -876
  309. package/core/server/services/members/csv/index.js +0 -10
  310. package/core/server/services/members/csv/index.ts +0 -3
  311. package/core/server/services/members/csv/parse.js +0 -94
  312. package/core/server/services/members/csv/parse.ts +0 -111
  313. package/core/server/services/members/csv/types.js +0 -2
  314. package/core/server/services/members/csv/types.ts +0 -39
  315. package/core/server/services/members/csv/unparse.js +0 -69
  316. package/core/server/services/members/csv/unparse.ts +0 -79
  317. package/core/server/services/members/exporter/query.js +0 -233
  318. package/core/server/services/members/importer/index.js +0 -30
  319. package/core/server/services/members/importer/labels.js +0 -125
  320. package/core/server/services/members/importer/members-csv-importer.js +0 -520
  321. package/core/server/services/route-settings/default-settings-manager.js +0 -63
  322. package/core/server/services/route-settings/route-settings.js +0 -170
  323. package/core/server/services/route-settings/settings-loader.js +0 -60
  324. package/core/server/services/route-settings/settings-path-manager.js +0 -47
  325. package/core/server/services/route-settings/validate.js +0 -440
  326. /package/core/server/services/members/{importer → import-export/import}/email-template.js +0 -0
  327. /package/core/server/services/members/{importer/members-csv-importer-stripe-utils.js → import-export/import/stripe-utils.js} +0 -0
@@ -0,0 +1,38 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import fs from 'fs-extra';
5
+ import type {MemberImportRow} from './row';
6
+
7
+ // A handle to rows written to a spool: the deferred job reads them back once it
8
+ // runs, then removes the file. Removal swallows errors -- a failed cleanup must
9
+ // never fail the import it was cleaning up after.
10
+ export interface SpooledRows {
11
+ read(): Promise<MemberImportRow[]>;
12
+ remove(): Promise<void>;
13
+ }
14
+
15
+ export interface RowSpool {
16
+ write(rows: MemberImportRow[]): Promise<SpooledRows>;
17
+ }
18
+
19
+ // Spools import rows to a private JSON file under the OS temp dir, so a deferred
20
+ // import can hand them to a background job and read them back after the request has
21
+ // already returned. The rows go in and come out as MemberImportRow, so nothing but
22
+ // the import's own row shape crosses this boundary.
23
+ export function createRowSpool(): RowSpool {
24
+ return {
25
+ async write(rows) {
26
+ const spoolPath = path.join(os.tmpdir(), `members-import-${crypto.randomUUID()}.json`);
27
+ await fs.writeFile(spoolPath, JSON.stringify(rows), {mode: 0o600});
28
+ return {
29
+ async read() {
30
+ return JSON.parse(await fs.readFile(spoolPath, 'utf8'));
31
+ },
32
+ async remove() {
33
+ await fs.remove(spoolPath).catch(() => {});
34
+ }
35
+ };
36
+ }
37
+ };
38
+ }
@@ -0,0 +1,99 @@
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.makeImporter = makeImporter;
7
+ exports.makeExporter = makeExporter;
8
+ const importer_1 = __importDefault(require("./import/importer"));
9
+ const reader_1 = __importDefault(require("./import/reader"));
10
+ const spool_1 = require("./import/spool");
11
+ const exporter_1 = __importDefault(require("./export/exporter"));
12
+ const MembersCSVImporterStripeUtils = require('./import/stripe-utils');
13
+ const db = require('../../../data/db');
14
+ const models = require('../../../models');
15
+ const labs = require('../../../../shared/labs');
16
+ // Build the members CSV importer. This is the composition root: today's models and
17
+ // services are wired behind the collaborators the importer declares, one per
18
+ // concern, so nothing Bookshelf-shaped leaks into the import service itself.
19
+ function makeImporter(deps) {
20
+ // The members repository resolves asynchronously and is stable once ready, so
21
+ // cache the promise and reuse it across every call the import makes.
22
+ let membersRepositoryPromise;
23
+ const getMembersRepository = () => (membersRepositoryPromise ??= deps.getMembersRepository());
24
+ // The members aggregate, plus the import label lookup folded in so the label
25
+ // (member-tagging data) does not need a source of its own.
26
+ const members = {
27
+ get: async (query, options) => (await getMembersRepository()).get(query, options),
28
+ create: async (values, options) => (await getMembersRepository()).create(values, options),
29
+ update: async (values, options) => (await getMembersRepository()).update(values, options),
30
+ getCustomerIdByEmail: async (email) => (await getMembersRepository()).getCustomerIdByEmail(email),
31
+ linkStripeCustomer: async (link, options) => (await getMembersRepository()).linkStripeCustomer(link, options),
32
+ getImportLabel: name => models.Label.findOne({ name })
33
+ };
34
+ // The completion email: its recipient, links and delivery in one collaborator.
35
+ const email = {
36
+ send: deps.sendEmail,
37
+ getDefaultRecipient: async () => (await models.User.getOwnerUser()).get('email'),
38
+ urlFor: deps.urlFor
39
+ };
40
+ // Gifts is initialised at boot and always present at request time; the getter
41
+ // resolves it lazily so the ready service is picked up whenever a row uses it.
42
+ const gifts = {
43
+ reassignRedeemer: (giftId, memberId, options) => deps.getGiftService().reassignRedeemer(giftId, memberId, options)
44
+ };
45
+ // Gated by the same labs flag as the export, so the two halves round-trip or stay
46
+ // silent together: off, activeFields resolves empty and every custom_fields.* column
47
+ // is dropped.
48
+ const customFields = {
49
+ activeFields: async () => (labs.isSet('membersCustomFields') ? deps.customFields.definitions.browse() : []),
50
+ planWrite: values => deps.customFields.values.planWrite(values),
51
+ applyWrite: (memberId, plan, executor) => deps.customFields.values.applyWrite(memberId, plan, executor)
52
+ };
53
+ return new importer_1.default({
54
+ knex: deps.knex,
55
+ readRows: reader_1.default,
56
+ spool: (0, spool_1.createRowSpool)(),
57
+ members,
58
+ tiers: {
59
+ getDefault: deps.getDefaultTier,
60
+ getByName: deps.getTierByName
61
+ },
62
+ stripe: new MembersCSVImporterStripeUtils({
63
+ stripeAPIService: deps.stripeAPIService,
64
+ productRepository: deps.productRepository
65
+ }),
66
+ gifts,
67
+ customFields,
68
+ email,
69
+ addJob: deps.addJob,
70
+ getTimezone: deps.getTimezone,
71
+ getInlineThreshold: deps.getInlineThreshold
72
+ });
73
+ }
74
+ // Build the members CSV exporter. The same composition root from the other direction:
75
+ // knex and the members id lookup are wired here, and the custom fields definitions and
76
+ // values services are injected (boot builds them before this one). The labs flag alone
77
+ // decides whether custom field columns appear, so nothing flag-shaped leaks into the
78
+ // exporter itself.
79
+ function makeExporter({ definitions, values }) {
80
+ const exporter = new exporter_1.default({
81
+ knex: db.knex,
82
+ members: {
83
+ // Minimal query, only to fetch the ids of the filtered members; the stream
84
+ // reads their related data itself.
85
+ findFilteredIds: async (options) => {
86
+ const page = await models.Member.findPage({ ...options, withRelated: [], columns: ['id'], limit: 'all' });
87
+ return page.data.map((member) => member.id);
88
+ }
89
+ },
90
+ customFields: {
91
+ // Boot builds the definitions and values services before this one, so they
92
+ // are always present -- no not-initialised state to guard. The flag decides
93
+ // whether their columns are included at all.
94
+ activeDefinitions: async () => (labs.isSet('membersCustomFields') ? definitions.browse() : []),
95
+ valuesForMembers: memberIds => values.getValuesForMembers(memberIds)
96
+ }
97
+ });
98
+ return (options = {}) => exporter.export(options);
99
+ }
@@ -0,0 +1,137 @@
1
+ import type {Knex} from 'knex';
2
+ import type {CsvField} from '@tryghost/custom-field-types/csv';
3
+ import MembersCSVImporter, {type MembersRepository, type GiftService, type EmailNotifications, type Tier, type CustomFieldsImport} from './import/importer';
4
+ import readMemberRows from './import/reader';
5
+ import {createRowSpool} from './import/spool';
6
+ import MembersCSVExporter, {type ExportOptions, type CustomFieldDefinition} from './export/exporter';
7
+
8
+ const MembersCSVImporterStripeUtils = require('./import/stripe-utils');
9
+ const db = require('../../../data/db');
10
+ const models = require('../../../models');
11
+ const labs = require('../../../../shared/labs');
12
+
13
+ // The raw collaborators the members service hands the import composition root, before
14
+ // they are adapted into the ports the importer declares. The members repository is the
15
+ // importer's aggregate minus the import-label lookup, which the root folds in itself.
16
+ interface ImporterServices {
17
+ knex: Knex;
18
+ getMembersRepository(): Promise<Omit<MembersRepository, 'getImportLabel'>>;
19
+ getDefaultTier(): Promise<Tier>;
20
+ getTierByName(name: string): Promise<Tier | null>;
21
+ getGiftService(): GiftService;
22
+ sendEmail: EmailNotifications['send'];
23
+ urlFor: EmailNotifications['urlFor'];
24
+ addJob(job: {job: () => Promise<void>; offloaded: boolean; name: string}): void;
25
+ getTimezone(): string;
26
+ getInlineThreshold(): number;
27
+ stripeAPIService: unknown;
28
+ productRepository: unknown;
29
+ // The custom fields services the members service hands the import composition root.
30
+ customFields: {
31
+ definitions: {browse(): Promise<CsvField[]>};
32
+ values: {
33
+ planWrite(values: Record<string, unknown>): Promise<unknown[]>;
34
+ applyWrite(memberId: string, plan: unknown[], executor: Knex): Promise<void>;
35
+ };
36
+ };
37
+ }
38
+
39
+ // The custom fields services the members service hands the export composition root.
40
+ interface CustomFieldsServices {
41
+ definitions: {browse(): Promise<CustomFieldDefinition[]>};
42
+ values: {getValuesForMembers(memberIds: string[]): Promise<Map<string, Record<string, unknown>>>};
43
+ }
44
+
45
+ // Build the members CSV importer. This is the composition root: today's models and
46
+ // services are wired behind the collaborators the importer declares, one per
47
+ // concern, so nothing Bookshelf-shaped leaks into the import service itself.
48
+ export function makeImporter(deps: ImporterServices) {
49
+ // The members repository resolves asynchronously and is stable once ready, so
50
+ // cache the promise and reuse it across every call the import makes.
51
+ let membersRepositoryPromise: Promise<Omit<MembersRepository, 'getImportLabel'>> | undefined;
52
+ const getMembersRepository = () => (membersRepositoryPromise ??= deps.getMembersRepository());
53
+
54
+ // The members aggregate, plus the import label lookup folded in so the label
55
+ // (member-tagging data) does not need a source of its own.
56
+ const members: MembersRepository = {
57
+ get: async (query, options) => (await getMembersRepository()).get(query, options),
58
+ create: async (values, options) => (await getMembersRepository()).create(values, options),
59
+ update: async (values, options) => (await getMembersRepository()).update(values, options),
60
+ getCustomerIdByEmail: async email => (await getMembersRepository()).getCustomerIdByEmail(email),
61
+ linkStripeCustomer: async (link, options) => (await getMembersRepository()).linkStripeCustomer(link, options),
62
+ getImportLabel: name => models.Label.findOne({name})
63
+ };
64
+
65
+ // The completion email: its recipient, links and delivery in one collaborator.
66
+ const email: EmailNotifications = {
67
+ send: deps.sendEmail,
68
+ getDefaultRecipient: async () => (await models.User.getOwnerUser()).get('email'),
69
+ urlFor: deps.urlFor
70
+ };
71
+
72
+ // Gifts is initialised at boot and always present at request time; the getter
73
+ // resolves it lazily so the ready service is picked up whenever a row uses it.
74
+ const gifts: GiftService = {
75
+ reassignRedeemer: (giftId, memberId, options) => deps.getGiftService().reassignRedeemer(giftId, memberId, options)
76
+ };
77
+
78
+ // Gated by the same labs flag as the export, so the two halves round-trip or stay
79
+ // silent together: off, activeFields resolves empty and every custom_fields.* column
80
+ // is dropped.
81
+ const customFields: CustomFieldsImport = {
82
+ activeFields: async () => (labs.isSet('membersCustomFields') ? deps.customFields.definitions.browse() : []),
83
+ planWrite: values => deps.customFields.values.planWrite(values),
84
+ applyWrite: (memberId, plan, executor) => deps.customFields.values.applyWrite(memberId, plan, executor)
85
+ };
86
+
87
+ return new MembersCSVImporter({
88
+ knex: deps.knex,
89
+ readRows: readMemberRows,
90
+ spool: createRowSpool(),
91
+ members,
92
+ tiers: {
93
+ getDefault: deps.getDefaultTier,
94
+ getByName: deps.getTierByName
95
+ },
96
+ stripe: new MembersCSVImporterStripeUtils({
97
+ stripeAPIService: deps.stripeAPIService,
98
+ productRepository: deps.productRepository
99
+ }),
100
+ gifts,
101
+ customFields,
102
+ email,
103
+ addJob: deps.addJob,
104
+ getTimezone: deps.getTimezone,
105
+ getInlineThreshold: deps.getInlineThreshold
106
+ });
107
+ }
108
+
109
+ // Build the members CSV exporter. The same composition root from the other direction:
110
+ // knex and the members id lookup are wired here, and the custom fields definitions and
111
+ // values services are injected (boot builds them before this one). The labs flag alone
112
+ // decides whether custom field columns appear, so nothing flag-shaped leaks into the
113
+ // exporter itself.
114
+ export function makeExporter({definitions, values}: CustomFieldsServices): (options?: ExportOptions) => Promise<NodeJS.ReadableStream> {
115
+ const exporter = new MembersCSVExporter({
116
+ knex: db.knex,
117
+
118
+ members: {
119
+ // Minimal query, only to fetch the ids of the filtered members; the stream
120
+ // reads their related data itself.
121
+ findFilteredIds: async (options) => {
122
+ const page = await models.Member.findPage({...options, withRelated: [], columns: ['id'], limit: 'all'});
123
+ return page.data.map((member: {id: string}) => member.id);
124
+ }
125
+ },
126
+
127
+ customFields: {
128
+ // Boot builds the definitions and values services before this one, so they
129
+ // are always present -- no not-initialised state to guard. The flag decides
130
+ // whether their columns are included at all.
131
+ activeDefinitions: async (): Promise<CustomFieldDefinition[]> => (labs.isSet('membersCustomFields') ? definitions.browse() : []),
132
+ valuesForMembers: memberIds => values.getValuesForMembers(memberIds)
133
+ }
134
+ });
135
+
136
+ return (options = {}) => exporter.export(options);
137
+ }
@@ -467,6 +467,13 @@ module.exports = class RouterController {
467
467
  });
468
468
  }
469
469
 
470
+ if (offer.status && offer.status !== 'active') {
471
+ throw new BadRequestError({
472
+ message: tpl(messages.offerArchived),
473
+ context: 'Offer with id "' + offerId + '" is no longer active'
474
+ });
475
+ }
476
+
470
477
  if (!offer.tier) {
471
478
  throw new BadRequestError({
472
479
  message: 'Offer does not have a tier'
@@ -4,13 +4,12 @@ const tpl = require('@tryghost/tpl');
4
4
  const MembersSSR = require('./members-ssr');
5
5
  const db = require('../../data/db');
6
6
  const MembersConfigProvider = require('./members-config-provider');
7
- const makeMembersCSVImporter = require('./importer');
8
- const {resolveInlineThreshold} = require('./importer/config');
7
+ const {makeImporter, makeExporter} = require('./import-export');
8
+ const {resolveInlineThreshold} = require('./import-export/config');
9
9
  const MembersStats = require('./stats/members-stats');
10
10
  const memberJobs = require('./jobs');
11
11
  const logging = require('@tryghost/logging');
12
12
  const urlUtils = require('../../../shared/url-utils').default;
13
- const labsService = require('../../../shared/labs');
14
13
  const settingsCache = require('../../../shared/settings-cache');
15
14
  const config = require('../../../shared/config');
16
15
  const models = require('../../models');
@@ -47,9 +46,11 @@ const membersStats = new MembersStats({
47
46
  let membersApi;
48
47
  let verificationTrigger;
49
48
 
50
- const initMembersCSVImporter = ({stripeAPIService}) => {
51
- return makeMembersCSVImporter({
52
- storagePath: config.getContentPath('data'),
49
+ const buildImporterDeps = ({stripeAPIService}) => {
50
+ // Required here, not statically: boot builds the custom fields services before this
51
+ // one (the exporter below relies on the same).
52
+ const customFields = require('../members-custom-fields');
53
+ return {
53
54
  getTimezone: () => settingsCache.get('timezone'),
54
55
  // A getter rather than a value because the threshold is an operator
55
56
  // setting that can change between requests
@@ -79,16 +80,16 @@ const initMembersCSVImporter = ({stripeAPIService}) => {
79
80
  },
80
81
  getGiftService: () => giftService.service,
81
82
  sendEmail: ghostMailer.send.bind(ghostMailer),
82
- isSet: flag => labsService.isSet(flag),
83
83
  addJob: jobsService.addJob.bind(jobsService),
84
84
  knex: db.knex,
85
85
  urlFor: urlUtils.urlFor.bind(urlUtils),
86
- context: {
87
- importer: true
88
- },
89
86
  stripeAPIService,
90
- productRepository: membersApi.productRepository
91
- });
87
+ productRepository: membersApi.productRepository,
88
+ customFields: {
89
+ definitions: customFields.definitions,
90
+ values: customFields.values
91
+ }
92
+ };
92
93
  };
93
94
 
94
95
  const initVerificationTrigger = () => {
@@ -154,19 +155,23 @@ module.exports = {
154
155
  }
155
156
  module.exports.verificationTrigger = verificationTrigger;
156
157
 
157
- const membersCSVImporter = initMembersCSVImporter({stripeAPIService: stripeService.api});
158
+ const importerDeps = buildImporterDeps({stripeAPIService: stripeService.api});
159
+ const membersCSVImporter = makeImporter(importerDeps);
160
+ // The importer takes plain arguments and returns the domain outcome; callers build
161
+ // the request and shape the response. importCSV decides inline vs deferred by load
162
+ // (the endpoint); importInline always runs now (the Revue data import). This only
163
+ // supplies the verification trigger, a members-service internal.
164
+ module.exports.importCSV = (request) => membersCSVImporter.importCSV(request, verificationTrigger);
165
+ module.exports.importInline = (request) => membersCSVImporter.importInline(request, verificationTrigger);
166
+
158
167
  // Constructed here rather than required statically: the exporter needs the
159
168
  // custom fields services, which boot builds before this one.
160
169
  const customFields = require('../members-custom-fields');
161
- module.exports.export = require('./exporter/query')({
170
+ module.exports.export = makeExporter({
162
171
  definitions: customFields.definitions,
163
172
  values: customFields.values
164
173
  });
165
174
 
166
- module.exports.processImport = async (options) => {
167
- return await membersCSVImporter.process({...options, verificationTrigger});
168
- };
169
-
170
175
  if (!env?.startsWith('testing')) {
171
176
  const membersMigrationJobName = 'members-migrations';
172
177
  if (!(await jobsService.hasExecutedSuccessfully(membersMigrationJobName))) {
@@ -205,10 +210,10 @@ module.exports = {
205
210
 
206
211
  stripeConnect: require('./stripe-connect'),
207
212
 
208
- processImport: null,
213
+ importCSV: null,
214
+ importInline: null,
209
215
 
210
216
  stats: membersStats,
211
-
212
217
  export: null
213
218
  };
214
219
 
@@ -137,8 +137,9 @@ class CustomFieldValuesService {
137
137
  /**
138
138
  * Resolve input into the writes it implies, rejecting anything invalid, and
139
139
  * writing nothing. Returned so a caller can validate before it commits to a
140
- * change it would have to unwind (the member edit validates up front), then
141
- * apply the same plan without re-resolving or re-validating.
140
+ * change it would have to unwind (the member edit and the importer both validate
141
+ * up front, before opening a transaction), then apply the same plan without
142
+ * re-resolving or re-validating.
142
143
  */
143
144
  async planWrite(input) {
144
145
  const values = this.parseValues(input);
@@ -191,14 +192,17 @@ class CustomFieldValuesService {
191
192
  * Apply a plan from `planWrite`.
192
193
  *
193
194
  * Merge, not replace: only the fields in the plan are touched, so a caller
194
- * that doesn't know about a field can't erase it. The whole plan is applied in
195
- * one transaction, so a mid-batch failure rolls the batch back.
195
+ * that doesn't know about a field can't erase it.
196
+ *
197
+ * Always transactional, so a mid-batch failure rolls the batch back. Passed an
198
+ * executor it joins that transaction -- the importer passes its per-member one, so a
199
+ * failed value write takes the member with it; passed nothing it opens its own.
196
200
  */
197
- async applyWrite(memberId, writes) {
201
+ async applyWrite(memberId, writes, executor = this.knex) {
198
202
  if (writes.length === 0) {
199
203
  return;
200
204
  }
201
- await this.knex.transaction(async (trx) => {
205
+ const apply = async (trx) => {
202
206
  for (const { field, value } of writes) {
203
207
  const target = { member_id: memberId, custom_field_id: field.id };
204
208
  if (value === undefined) {
@@ -213,7 +217,15 @@ class CustomFieldValuesService {
213
217
  .onConflict(['member_id', 'custom_field_id'])
214
218
  .merge({ ...valueColumns, updated_at: new Date() });
215
219
  }
216
- });
220
+ };
221
+ // isTransaction is knex's marker for a transactor: join an existing transaction
222
+ // rather than nesting a savepoint under it, otherwise open one.
223
+ if (executor.isTransaction) {
224
+ await apply(executor);
225
+ }
226
+ else {
227
+ await executor.transaction(apply);
228
+ }
217
229
  }
218
230
  }
219
231
  exports.CustomFieldValuesService = CustomFieldValuesService;
@@ -164,8 +164,9 @@ export class CustomFieldValuesService {
164
164
  /**
165
165
  * Resolve input into the writes it implies, rejecting anything invalid, and
166
166
  * writing nothing. Returned so a caller can validate before it commits to a
167
- * change it would have to unwind (the member edit validates up front), then
168
- * apply the same plan without re-resolving or re-validating.
167
+ * change it would have to unwind (the member edit and the importer both validate
168
+ * up front, before opening a transaction), then apply the same plan without
169
+ * re-resolving or re-validating.
169
170
  */
170
171
  async planWrite(input: unknown): Promise<PlannedWrite[]> {
171
172
  const values = this.parseValues(input);
@@ -225,15 +226,18 @@ export class CustomFieldValuesService {
225
226
  * Apply a plan from `planWrite`.
226
227
  *
227
228
  * Merge, not replace: only the fields in the plan are touched, so a caller
228
- * that doesn't know about a field can't erase it. The whole plan is applied in
229
- * one transaction, so a mid-batch failure rolls the batch back.
229
+ * that doesn't know about a field can't erase it.
230
+ *
231
+ * Always transactional, so a mid-batch failure rolls the batch back. Passed an
232
+ * executor it joins that transaction -- the importer passes its per-member one, so a
233
+ * failed value write takes the member with it; passed nothing it opens its own.
230
234
  */
231
- async applyWrite(memberId: string, writes: PlannedWrite[]): Promise<void> {
235
+ async applyWrite(memberId: string, writes: PlannedWrite[], executor: Knex = this.knex): Promise<void> {
232
236
  if (writes.length === 0) {
233
237
  return;
234
238
  }
235
239
 
236
- await this.knex.transaction(async (trx) => {
240
+ const apply = async (trx: Knex) => {
237
241
  for (const {field, value} of writes) {
238
242
  const target = {member_id: memberId, custom_field_id: field.id};
239
243
 
@@ -250,6 +254,14 @@ export class CustomFieldValuesService {
250
254
  .onConflict(['member_id', 'custom_field_id'])
251
255
  .merge({...valueColumns, updated_at: new Date()});
252
256
  }
253
- });
257
+ };
258
+
259
+ // isTransaction is knex's marker for a transactor: join an existing transaction
260
+ // rather than nesting a savepoint under it, otherwise open one.
261
+ if (executor.isTransaction) {
262
+ await apply(executor);
263
+ } else {
264
+ await executor.transaction(apply);
265
+ }
254
266
  }
255
267
  }
@@ -29,7 +29,11 @@ async function getPostData(post) {
29
29
 
30
30
  function getPostUrl(id, postData) {
31
31
  const jsonModel = {...postData};
32
- outputSerializerUrlUtil.forPost(id, jsonModel, {options: {}});
32
+ // The URL service routes by resource type. Pages and posts share the Post
33
+ // model, so the page's own type must reach forPost — otherwise it defaults
34
+ // to 'posts', matches no post collection, and 404s under the lazy service.
35
+ const type = postData.type === 'page' ? 'pages' : 'posts';
36
+ outputSerializerUrlUtil.forPost(id, jsonModel, {options: {}}, type);
33
37
  return jsonModel.url;
34
38
  }
35
39
 
@@ -83,7 +83,7 @@ class PostsExporter {
83
83
  * @param {string|number} [options.limit]
84
84
  * @returns {Promise<Readable>}
85
85
  */
86
- async export({filter, order, limit}) {
86
+ async export({filter, order, limit, mongoTransformer}) {
87
87
  const exportContext = await this.#getExportContext();
88
88
  const requestedLimit = parseExportLimit(limit);
89
89
  const pageLimit = Math.min(EXPORT_BATCH_SIZE, requestedLimit);
@@ -93,7 +93,8 @@ class PostsExporter {
93
93
  order: withStableExportOrder(order),
94
94
  requestedLimit,
95
95
  pageLimit,
96
- exportContext
96
+ exportContext,
97
+ mongoTransformer
97
98
  }), {objectMode: true});
98
99
  }
99
100
 
@@ -119,7 +120,7 @@ class PostsExporter {
119
120
  * @param {ExportContext} options.exportContext
120
121
  * @returns {AsyncGenerator<object>}
121
122
  */
122
- async *#streamPosts({filter, order, requestedLimit, pageLimit, exportContext}) {
123
+ async *#streamPosts({filter, order, requestedLimit, pageLimit, exportContext, mongoTransformer}) {
123
124
  let emitted = 0;
124
125
 
125
126
  for (let page = 1; emitted < requestedLimit; page++) {
@@ -129,7 +130,8 @@ class PostsExporter {
129
130
  limit: pageLimit,
130
131
  page,
131
132
  skipPagination: true,
132
- withRelated: EXPORT_WITH_RELATED
133
+ withRelated: EXPORT_WITH_RELATED,
134
+ mongoTransformer
133
135
  });
134
136
 
135
137
  if (posts.data.length === 0) {
@@ -329,8 +329,8 @@ class PostsService {
329
329
  return result;
330
330
  }
331
331
 
332
- async export(frame) {
333
- return await this.postsExporter.export(frame.options);
332
+ async export(options) {
333
+ return await this.postsExporter.export(options);
334
334
  }
335
335
 
336
336
  async #updatePosts(data, options) {