ghost 5.62.0 → 5.63.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 (157) hide show
  1. package/components/tryghost-adapter-cache-memory-ttl-5.63.0.tgz +0 -0
  2. package/components/{tryghost-adapter-cache-redis-5.62.0.tgz → tryghost-adapter-cache-redis-5.63.0.tgz} +0 -0
  3. package/components/{tryghost-adapter-manager-5.62.0.tgz → tryghost-adapter-manager-5.63.0.tgz} +0 -0
  4. package/components/{tryghost-announcement-bar-settings-5.62.0.tgz → tryghost-announcement-bar-settings-5.63.0.tgz} +0 -0
  5. package/components/{tryghost-api-framework-5.62.0.tgz → tryghost-api-framework-5.63.0.tgz} +0 -0
  6. package/components/{tryghost-api-version-compatibility-service-5.62.0.tgz → tryghost-api-version-compatibility-service-5.63.0.tgz} +0 -0
  7. package/components/{tryghost-audience-feedback-5.62.0.tgz → tryghost-audience-feedback-5.63.0.tgz} +0 -0
  8. package/components/tryghost-bookshelf-repository-5.63.0.tgz +0 -0
  9. package/components/{tryghost-bootstrap-socket-5.62.0.tgz → tryghost-bootstrap-socket-5.63.0.tgz} +0 -0
  10. package/components/tryghost-collections-5.63.0.tgz +0 -0
  11. package/components/{tryghost-constants-5.62.0.tgz → tryghost-constants-5.63.0.tgz} +0 -0
  12. package/components/tryghost-custom-theme-settings-service-5.63.0.tgz +0 -0
  13. package/components/{tryghost-data-generator-5.62.0.tgz → tryghost-data-generator-5.63.0.tgz} +0 -0
  14. package/components/{tryghost-domain-events-5.62.0.tgz → tryghost-domain-events-5.63.0.tgz} +0 -0
  15. package/components/{tryghost-donations-5.62.0.tgz → tryghost-donations-5.63.0.tgz} +0 -0
  16. package/components/tryghost-dynamic-routing-events-5.63.0.tgz +0 -0
  17. package/components/tryghost-email-analytics-provider-mailgun-5.63.0.tgz +0 -0
  18. package/components/{tryghost-email-analytics-service-5.62.0.tgz → tryghost-email-analytics-service-5.63.0.tgz} +0 -0
  19. package/components/{tryghost-email-content-generator-5.62.0.tgz → tryghost-email-content-generator-5.63.0.tgz} +0 -0
  20. package/components/tryghost-email-events-5.63.0.tgz +0 -0
  21. package/components/{tryghost-email-service-5.62.0.tgz → tryghost-email-service-5.63.0.tgz} +0 -0
  22. package/components/tryghost-email-suppression-list-5.63.0.tgz +0 -0
  23. package/components/tryghost-event-aware-cache-wrapper-5.63.0.tgz +0 -0
  24. package/components/tryghost-express-dynamic-redirects-5.63.0.tgz +0 -0
  25. package/components/{tryghost-external-media-inliner-5.62.0.tgz → tryghost-external-media-inliner-5.63.0.tgz} +0 -0
  26. package/components/{tryghost-extract-api-key-5.62.0.tgz → tryghost-extract-api-key-5.63.0.tgz} +0 -0
  27. package/components/{tryghost-html-to-plaintext-5.62.0.tgz → tryghost-html-to-plaintext-5.63.0.tgz} +0 -0
  28. package/components/tryghost-i18n-5.63.0.tgz +0 -0
  29. package/components/tryghost-importer-handler-content-files-5.63.0.tgz +0 -0
  30. package/components/{tryghost-importer-revue-5.62.0.tgz → tryghost-importer-revue-5.63.0.tgz} +0 -0
  31. package/components/{tryghost-in-memory-repository-5.62.0.tgz → tryghost-in-memory-repository-5.63.0.tgz} +0 -0
  32. package/components/{tryghost-job-manager-5.62.0.tgz → tryghost-job-manager-5.63.0.tgz} +0 -0
  33. package/components/{tryghost-link-redirects-5.62.0.tgz → tryghost-link-redirects-5.63.0.tgz} +0 -0
  34. package/components/{tryghost-link-replacer-5.62.0.tgz → tryghost-link-replacer-5.63.0.tgz} +0 -0
  35. package/components/{tryghost-link-tracking-5.62.0.tgz → tryghost-link-tracking-5.63.0.tgz} +0 -0
  36. package/components/tryghost-magic-link-5.63.0.tgz +0 -0
  37. package/components/{tryghost-mail-events-5.62.0.tgz → tryghost-mail-events-5.63.0.tgz} +0 -0
  38. package/components/{tryghost-mailgun-client-5.62.0.tgz → tryghost-mailgun-client-5.63.0.tgz} +0 -0
  39. package/components/{tryghost-member-attribution-5.62.0.tgz → tryghost-member-attribution-5.63.0.tgz} +0 -0
  40. package/components/{tryghost-member-events-5.62.0.tgz → tryghost-member-events-5.63.0.tgz} +0 -0
  41. package/components/tryghost-members-api-5.63.0.tgz +0 -0
  42. package/components/{tryghost-members-csv-5.62.0.tgz → tryghost-members-csv-5.63.0.tgz} +0 -0
  43. package/components/{tryghost-members-events-service-5.62.0.tgz → tryghost-members-events-service-5.63.0.tgz} +0 -0
  44. package/components/{tryghost-members-importer-5.62.0.tgz → tryghost-members-importer-5.63.0.tgz} +0 -0
  45. package/components/{tryghost-members-offers-5.62.0.tgz → tryghost-members-offers-5.63.0.tgz} +0 -0
  46. package/components/{tryghost-members-payments-5.62.0.tgz → tryghost-members-payments-5.63.0.tgz} +0 -0
  47. package/components/tryghost-members-ssr-5.63.0.tgz +0 -0
  48. package/components/{tryghost-members-stripe-service-5.62.0.tgz → tryghost-members-stripe-service-5.63.0.tgz} +0 -0
  49. package/components/tryghost-mentions-email-report-5.63.0.tgz +0 -0
  50. package/components/tryghost-milestones-5.63.0.tgz +0 -0
  51. package/components/{tryghost-minifier-5.62.0.tgz → tryghost-minifier-5.63.0.tgz} +0 -0
  52. package/components/tryghost-model-to-domain-event-interceptor-5.63.0.tgz +0 -0
  53. package/components/tryghost-mw-api-version-mismatch-5.63.0.tgz +0 -0
  54. package/components/tryghost-mw-cache-control-5.63.0.tgz +0 -0
  55. package/components/{tryghost-mw-error-handler-5.62.0.tgz → tryghost-mw-error-handler-5.63.0.tgz} +0 -0
  56. package/components/tryghost-mw-session-from-token-5.63.0.tgz +0 -0
  57. package/components/tryghost-mw-update-user-last-seen-5.63.0.tgz +0 -0
  58. package/components/tryghost-mw-version-match-5.63.0.tgz +0 -0
  59. package/components/tryghost-mw-vhost-5.63.0.tgz +0 -0
  60. package/components/{tryghost-nql-filter-expansions-5.62.0.tgz → tryghost-nql-filter-expansions-5.63.0.tgz} +0 -0
  61. package/components/tryghost-oembed-service-5.63.0.tgz +0 -0
  62. package/components/{tryghost-package-json-5.62.0.tgz → tryghost-package-json-5.63.0.tgz} +0 -0
  63. package/components/tryghost-post-events-5.63.0.tgz +0 -0
  64. package/components/tryghost-post-revisions-5.63.0.tgz +0 -0
  65. package/components/tryghost-posts-service-5.63.0.tgz +0 -0
  66. package/components/tryghost-recommendations-5.63.0.tgz +0 -0
  67. package/components/tryghost-referrers-5.63.0.tgz +0 -0
  68. package/components/{tryghost-security-5.62.0.tgz → tryghost-security-5.63.0.tgz} +0 -0
  69. package/components/tryghost-session-service-5.63.0.tgz +0 -0
  70. package/components/tryghost-settings-path-manager-5.63.0.tgz +0 -0
  71. package/components/tryghost-slack-notifications-5.63.0.tgz +0 -0
  72. package/components/{tryghost-staff-service-5.62.0.tgz → tryghost-staff-service-5.63.0.tgz} +0 -0
  73. package/components/tryghost-stats-service-5.63.0.tgz +0 -0
  74. package/components/{tryghost-tiers-5.62.0.tgz → tryghost-tiers-5.63.0.tgz} +0 -0
  75. package/components/{tryghost-update-check-service-5.62.0.tgz → tryghost-update-check-service-5.63.0.tgz} +0 -0
  76. package/components/tryghost-verification-trigger-5.63.0.tgz +0 -0
  77. package/components/{tryghost-version-notifications-data-service-5.62.0.tgz → tryghost-version-notifications-data-service-5.63.0.tgz} +0 -0
  78. package/components/{tryghost-webmentions-5.62.0.tgz → tryghost-webmentions-5.63.0.tgz} +0 -0
  79. package/core/built/admin/assets/{chunk.143.5b5502a550ce35005d0f.js → chunk.143.1c5d21facf1f9b9beef9.js} +5 -5
  80. package/core/built/admin/assets/{chunk.178.546664edca0b1b0f2ab2.js → chunk.178.43b03c9ac011a54262b0.js} +4 -4
  81. package/core/built/admin/assets/{chunk.853.f743ed975e8838475532.js → chunk.237.9b7032162949850f6c76.js} +690 -678
  82. package/core/built/admin/assets/{ghost-8a4e981c272f793157133814ca7c7e84.css → ghost-33664cad4cd6664a8b5fa56e62c5005f.css} +1 -1
  83. package/core/built/admin/assets/{ghost-d804aba7bca07fa75d308ab892c508fc.js → ghost-7c3f2de2ec83e591ad9f9db5165b2733.js} +62 -57
  84. package/core/built/admin/assets/ghost-dark-0452daeaee3a9b16dcd954ea60dad518.css +1 -0
  85. package/core/built/admin/assets/{vendor-b50a3e5c2079b8a35d9122a1a4c34ef6.js → vendor-f8ce8bd43cf5dad6608f828ab48cee9b.js} +2 -2
  86. package/core/built/admin/index.html +6 -6
  87. package/core/frontend/helpers/foreach.js +8 -0
  88. package/core/frontend/helpers/get.js +3 -0
  89. package/core/frontend/meta/schema.js +1 -1
  90. package/core/frontend/services/data/checks.js +6 -0
  91. package/core/frontend/src/cards/css/collection.css +186 -0
  92. package/core/frontend/src/cards/css/header_v2.css +19 -23
  93. package/core/frontend/src/cards/css/signup.css +20 -23
  94. package/core/server/api/endpoints/recommendations-public.js +42 -0
  95. package/core/server/api/endpoints/recommendations.js +2 -1
  96. package/core/server/api/endpoints/utils/serializers/output/members.js +22 -5
  97. package/core/server/data/exporter/table-lists.js +3 -1
  98. package/core/server/data/migrations/versions/5.63/2023-09-12-11-22-10-add-recommendation-click-events-table.js +8 -0
  99. package/core/server/data/migrations/versions/5.63/2023-09-12-11-22-11-add-recommendation-subscribe-events-table.js +8 -0
  100. package/core/server/data/migrations/versions/5.63/2023-09-13-13-03-10-add-ghost-core-content-integration.js +57 -0
  101. package/core/server/data/migrations/versions/5.63/2023-09-13-13-34-11-add-ghost-core-content-integration-key.js +87 -0
  102. package/core/server/data/schema/fixtures/fixtures.json +7 -0
  103. package/core/server/data/schema/schema.js +12 -0
  104. package/core/server/lib/lexical.js +20 -1
  105. package/core/server/models/base/plugins/crud.js +0 -6
  106. package/core/server/models/base/plugins/sanitize.js +1 -1
  107. package/core/server/models/post.js +1 -1
  108. package/core/server/models/recommendation-click-event.js +22 -0
  109. package/core/server/models/recommendation-subscribe-event.js +22 -0
  110. package/core/server/services/collections/BookshelfCollectionsRepository.js +67 -5
  111. package/core/server/services/collections/service.js +8 -8
  112. package/core/server/services/members/middleware.js +14 -1
  113. package/core/server/services/members/utils.js +1 -0
  114. package/core/server/services/recommendations/RecommendationServiceWrapper.js +23 -2
  115. package/core/server/services/settings/SettingsBREADService.js +5 -0
  116. package/core/server/web/members/app.js +14 -0
  117. package/core/shared/config/defaults.json +1 -1
  118. package/core/shared/settings-cache/public.js +2 -1
  119. package/package.json +159 -159
  120. package/yarn.lock +381 -369
  121. package/components/tryghost-adapter-cache-memory-ttl-5.62.0.tgz +0 -0
  122. package/components/tryghost-bookshelf-repository-5.62.0.tgz +0 -0
  123. package/components/tryghost-collections-5.62.0.tgz +0 -0
  124. package/components/tryghost-custom-theme-settings-service-5.62.0.tgz +0 -0
  125. package/components/tryghost-dynamic-routing-events-5.62.0.tgz +0 -0
  126. package/components/tryghost-email-analytics-provider-mailgun-5.62.0.tgz +0 -0
  127. package/components/tryghost-email-events-5.62.0.tgz +0 -0
  128. package/components/tryghost-email-suppression-list-5.62.0.tgz +0 -0
  129. package/components/tryghost-event-aware-cache-wrapper-5.62.0.tgz +0 -0
  130. package/components/tryghost-express-dynamic-redirects-5.62.0.tgz +0 -0
  131. package/components/tryghost-i18n-5.62.0.tgz +0 -0
  132. package/components/tryghost-importer-handler-content-files-5.62.0.tgz +0 -0
  133. package/components/tryghost-magic-link-5.62.0.tgz +0 -0
  134. package/components/tryghost-members-api-5.62.0.tgz +0 -0
  135. package/components/tryghost-members-ssr-5.62.0.tgz +0 -0
  136. package/components/tryghost-mentions-email-report-5.62.0.tgz +0 -0
  137. package/components/tryghost-milestones-5.62.0.tgz +0 -0
  138. package/components/tryghost-model-to-domain-event-interceptor-5.62.0.tgz +0 -0
  139. package/components/tryghost-mw-api-version-mismatch-5.62.0.tgz +0 -0
  140. package/components/tryghost-mw-cache-control-5.62.0.tgz +0 -0
  141. package/components/tryghost-mw-session-from-token-5.62.0.tgz +0 -0
  142. package/components/tryghost-mw-update-user-last-seen-5.62.0.tgz +0 -0
  143. package/components/tryghost-mw-version-match-5.62.0.tgz +0 -0
  144. package/components/tryghost-mw-vhost-5.62.0.tgz +0 -0
  145. package/components/tryghost-oembed-service-5.62.0.tgz +0 -0
  146. package/components/tryghost-post-events-5.62.0.tgz +0 -0
  147. package/components/tryghost-post-revisions-5.62.0.tgz +0 -0
  148. package/components/tryghost-posts-service-5.62.0.tgz +0 -0
  149. package/components/tryghost-recommendations-5.62.0.tgz +0 -0
  150. package/components/tryghost-referrers-5.62.0.tgz +0 -0
  151. package/components/tryghost-session-service-5.62.0.tgz +0 -0
  152. package/components/tryghost-settings-path-manager-5.62.0.tgz +0 -0
  153. package/components/tryghost-slack-notifications-5.62.0.tgz +0 -0
  154. package/components/tryghost-stats-service-5.62.0.tgz +0 -0
  155. package/components/tryghost-verification-trigger-5.62.0.tgz +0 -0
  156. package/core/built/admin/assets/ghost-dark-084169b0e968ef763dfbbf63b253e0c6.css +0 -1
  157. /package/core/built/admin/assets/{chunk.853.f743ed975e8838475532.js.LICENSE.txt → chunk.237.9b7032162949850f6c76.js.LICENSE.txt} +0 -0
@@ -0,0 +1,57 @@
1
+ // For information on writing migrations, see https://www.notion.so/ghost/Database-migrations-eb5b78c435d741d2b34a582d57c24253
2
+
3
+ const logging = require('@tryghost/logging');
4
+ const {default: ObjectID} = require('bson-objectid');
5
+ const {createTransactionalMigration, meta} = require('../../utils');
6
+
7
+ const coreContentIntegration = {
8
+ slug: 'ghost-core-content',
9
+ name: 'Ghost Core Content API',
10
+ description: 'Internal Content API integration for Admin access',
11
+ type: 'core'
12
+ };
13
+
14
+ const addIntegration = async (knex, integration) => {
15
+ const message = `Adding "${integration.name}" integration`;
16
+
17
+ const existing = await knex('integrations').select('id').where('slug', integration.slug).first();
18
+
19
+ if (existing?.id) {
20
+ logging.warn(`Skipping ${message} - already exists`);
21
+ return;
22
+ }
23
+
24
+ logging.info(message);
25
+
26
+ const now = knex.raw('CURRENT_TIMESTAMP');
27
+ integration.id = (new ObjectID()).toHexString();
28
+ integration.created_at = now;
29
+ integration.created_by = meta.MIGRATION_USER;
30
+
31
+ await knex('integrations').insert(integration);
32
+ };
33
+
34
+ const removeIntegration = async (knex, integration) => {
35
+ const message = `Removing ${integration.name} integration`;
36
+
37
+ const existing = await knex('integrations').select('id').where('slug', integration.slug).first();
38
+
39
+ if (!existing?.id) {
40
+ logging.warn(`Skipping ${message} - doesn't exist`);
41
+ return;
42
+ }
43
+
44
+ logging.info(message);
45
+
46
+ await knex('api_keys').where('integration_id', existing.id).del();
47
+ await knex('integrations').where('id', existing.id).del();
48
+ };
49
+
50
+ module.exports = createTransactionalMigration(
51
+ async function up(knex) {
52
+ await addIntegration(knex, coreContentIntegration);
53
+ },
54
+ async function down(knex) {
55
+ await removeIntegration(knex, coreContentIntegration);
56
+ }
57
+ );
@@ -0,0 +1,87 @@
1
+ // For information on writing migrations, see https://www.notion.so/ghost/Database-migrations-eb5b78c435d741d2b34a582d57c24253
2
+
3
+ const {InternalServerError} = require('@tryghost/errors');
4
+ const logging = require('@tryghost/logging');
5
+ const security = require('@tryghost/security');
6
+ const {default: ObjectID} = require('bson-objectid');
7
+ const {createTransactionalMigration, meta} = require('../../utils');
8
+
9
+ const coreContentIntegration = {
10
+ slug: 'ghost-core-content',
11
+ name: 'Ghost Core Content API',
12
+ description: 'Internal Content API integration for Admin access',
13
+ type: 'core'
14
+ };
15
+
16
+ const addIntegrationContentKey = async (knex, integration) => {
17
+ const message = `Adding "${integration.name}" integration content key`;
18
+
19
+ const existingIntegration = await knex('integrations').select('id').where({
20
+ slug: integration.slug
21
+ }).first();
22
+
23
+ if (!existingIntegration) {
24
+ throw new InternalServerError({
25
+ message: `Could not find "${integration.name}" integration`
26
+ });
27
+ }
28
+
29
+ const existing = await knex('api_keys').select('id')
30
+ .where('integration_id', existingIntegration.id)
31
+ .where('type', 'content')
32
+ .first();
33
+
34
+ if (existing?.id) {
35
+ logging.warn(`Skipping ${message} - already exists`);
36
+ return;
37
+ }
38
+
39
+ logging.info(message);
40
+
41
+ await knex('api_keys').insert({
42
+ id: (new ObjectID()).toHexString(),
43
+ type: 'content',
44
+ secret: security.secret.create('content'),
45
+ role_id: null,
46
+ integration_id: existingIntegration.id,
47
+ created_at: knex.raw('current_timestamp'),
48
+ created_by: meta.MIGRATION_USER
49
+ });
50
+ };
51
+
52
+ const removeIntegrationContentKey = async (knex, integration) => {
53
+ const message = `Removing "${integration.name}" integration content key`;
54
+
55
+ const existingIntegration = await knex('integrations').select('id').where({
56
+ slug: integration.slug
57
+ }).first();
58
+
59
+ if (!existingIntegration?.id) {
60
+ logging.warn(`Skipping ${message} - integration does not exist`);
61
+ return;
62
+ }
63
+
64
+ const existing = await knex('api_keys').select('id').where({
65
+ integration_id: existingIntegration.id,
66
+ type: 'content'
67
+ }).first();
68
+
69
+ if (!existing?.id) {
70
+ logging.warn(`Skipping ${message} - content key does not exist`);
71
+ return;
72
+ }
73
+
74
+ logging.info(message);
75
+
76
+ await knex('api_keys').where('id', existing.id).del();
77
+ };
78
+
79
+ module.exports = createTransactionalMigration(
80
+ async function up(knex) {
81
+ await addIntegrationContentKey(knex, coreContentIntegration);
82
+ },
83
+
84
+ async function down(knex) {
85
+ await removeIntegrationContentKey(knex, coreContentIntegration);
86
+ }
87
+ );
@@ -807,6 +807,13 @@
807
807
  "description": "Internal frontend integration",
808
808
  "type": "internal",
809
809
  "api_keys": [{"type": "content"}]
810
+ },
811
+ {
812
+ "slug": "ghost-core-content",
813
+ "name": "Ghost Core Content API",
814
+ "description": "Internal Content API integration for Admin access",
815
+ "type": "core",
816
+ "api_keys": [{"type": "content"}]
810
817
  }
811
818
  ]
812
819
  }
@@ -1079,5 +1079,17 @@ module.exports = {
1079
1079
  one_click_subscribe: {type: 'boolean', nullable: false, defaultTo: false},
1080
1080
  created_at: {type: 'dateTime', nullable: false},
1081
1081
  updated_at: {type: 'dateTime', nullable: true}
1082
+ },
1083
+ recommendation_click_events: {
1084
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
1085
+ recommendation_id: {type: 'string', maxlength: 24, nullable: false, references: 'recommendations.id', unique: false, cascadeDelete: true},
1086
+ member_id: {type: 'string', maxlength: 24, nullable: true, references: 'members.id', unique: false, setNullDelete: true},
1087
+ created_at: {type: 'dateTime', nullable: false}
1088
+ },
1089
+ recommendation_subscribe_events: {
1090
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
1091
+ recommendation_id: {type: 'string', maxlength: 24, nullable: false, references: 'recommendations.id', unique: false, cascadeDelete: true},
1092
+ member_id: {type: 'string', maxlength: 24, nullable: true, references: 'members.id', unique: false, setNullDelete: true},
1093
+ created_at: {type: 'dateTime', nullable: false}
1082
1094
  }
1083
1095
  };
@@ -7,6 +7,7 @@ const storage = require('../adapters/storage');
7
7
  let nodes;
8
8
  let lexicalHtmlRenderer;
9
9
  let urlTransformMap;
10
+ let postsService;
10
11
 
11
12
  function populateNodes() {
12
13
  const {DEFAULT_NODES} = require('@tryghost/kg-default-nodes');
@@ -28,6 +29,23 @@ module.exports = {
28
29
  },
29
30
 
30
31
  async render(lexical, userOptions = {}) {
32
+ if (!postsService) {
33
+ const getPostServiceInstance = require('../services/posts/posts-service');
34
+ postsService = getPostServiceInstance();
35
+ }
36
+
37
+ const getCollectionPosts = async (collectionSlug, postCount) => {
38
+ const transacting = userOptions.transacting;
39
+ const {data} = await postsService.browsePosts({
40
+ context: {public: true}, // mimic Content API request
41
+ collection: collectionSlug,
42
+ limit: postCount,
43
+ transacting
44
+ });
45
+ let posts = data.map(p => p.toJSON());
46
+ return posts;
47
+ };
48
+
31
49
  const options = Object.assign({
32
50
  siteUrl: config.get('url'),
33
51
  imageOptimization: config.get('imageOptimization'),
@@ -43,7 +61,8 @@ module.exports = {
43
61
  createDocument() {
44
62
  const {JSDOM} = require('jsdom');
45
63
  return (new JSDOM()).window.document;
46
- }
64
+ },
65
+ getCollectionPosts
47
66
  }, userOptions);
48
67
 
49
68
  return await this.lexicalHtmlRenderer.render(lexical, options);
@@ -44,12 +44,6 @@ module.exports = function (Bookshelf) {
44
44
  });
45
45
  }
46
46
 
47
- if (options.page && options.limit) {
48
- itemCollection
49
- .query('limit', options.limit)
50
- .query('offset', options.limit * (options.page - 1));
51
- }
52
-
53
47
  const result = await itemCollection.fetchAll(options);
54
48
  if (options.withRelated) {
55
49
  _.each(result.models, function each(item) {
@@ -45,7 +45,7 @@ module.exports = function (Bookshelf) {
45
45
  case 'findOne':
46
46
  return baseOptions.concat(extraOptions, ['columns', 'require', 'mongoTransformer']);
47
47
  case 'findAll':
48
- return baseOptions.concat(extraOptions, ['filter', 'columns', 'mongoTransformer', 'page', 'limit']);
48
+ return baseOptions.concat(extraOptions, ['filter', 'columns', 'mongoTransformer']);
49
49
  case 'findPage':
50
50
  return baseOptions.concat(extraOptions, ['filter', 'order', 'autoOrder', 'page', 'limit', 'columns', 'mongoTransformer']);
51
51
  default:
@@ -700,7 +700,7 @@ Post = ghostBookshelf.Model.extend({
700
700
  )
701
701
  ) {
702
702
  try {
703
- this.set('html', await lexicalLib.render(this.get('lexical')));
703
+ this.set('html', await lexicalLib.render(this.get('lexical'), {transacting: options.transacting}));
704
704
  } catch (err) {
705
705
  throw new errors.ValidationError({
706
706
  message: tpl(messages.invalidLexicalStructure),
@@ -0,0 +1,22 @@
1
+ const errors = require('@tryghost/errors');
2
+ const ghostBookshelf = require('./base');
3
+
4
+ const RecommendationClickEvent = ghostBookshelf.Model.extend({
5
+ tableName: 'recommendation_click_events'
6
+ }, {
7
+ async edit() {
8
+ throw new errors.IncorrectUsageError({
9
+ message: 'Cannot edit RecommendationClickEvent'
10
+ });
11
+ },
12
+
13
+ async destroy() {
14
+ throw new errors.IncorrectUsageError({
15
+ message: 'Cannot destroy RecommendationClickEvent'
16
+ });
17
+ }
18
+ });
19
+
20
+ module.exports = {
21
+ RecommendationClickEvent: ghostBookshelf.model('RecommendationClickEvent', RecommendationClickEvent)
22
+ };
@@ -0,0 +1,22 @@
1
+ const errors = require('@tryghost/errors');
2
+ const ghostBookshelf = require('./base');
3
+
4
+ const RecommendationSubscribeEvent = ghostBookshelf.Model.extend({
5
+ tableName: 'recommendation_subscribe_events'
6
+ }, {
7
+ async edit() {
8
+ throw new errors.IncorrectUsageError({
9
+ message: 'Cannot edit RecommendationSubscribeEvent'
10
+ });
11
+ },
12
+
13
+ async destroy() {
14
+ throw new errors.IncorrectUsageError({
15
+ message: 'Cannot destroy RecommendationSubscribeEvent'
16
+ });
17
+ }
18
+ });
19
+
20
+ module.exports = {
21
+ RecommendationSubscribeEvent: ghostBookshelf.model('RecommendationSubscribeEvent', RecommendationSubscribeEvent)
22
+ };
@@ -1,6 +1,7 @@
1
1
  const logger = require('@tryghost/logging');
2
2
  const Collection = require('@tryghost/collections').Collection;
3
3
  const sentry = require('../../../shared/sentry');
4
+ const {default: ObjectID} = require('bson-objectid');
4
5
  /**
5
6
  * @typedef {import('@tryghost/collections/src/CollectionRepository')} CollectionRepository
6
7
  */
@@ -10,8 +11,10 @@ const sentry = require('../../../shared/sentry');
10
11
  */
11
12
  module.exports = class BookshelfCollectionsRepository {
12
13
  #model;
13
- constructor(model) {
14
+ #relationModel;
15
+ constructor(model, relationModel) {
14
16
  this.#model = model;
17
+ this.#relationModel = relationModel;
15
18
  }
16
19
 
17
20
  async createTransaction(cb) {
@@ -101,10 +104,21 @@ module.exports = class BookshelfCollectionsRepository {
101
104
  * @returns {Promise<void>}
102
105
  */
103
106
  async save(collection, options = {}) {
107
+ if (!options.transaction) {
108
+ return this.createTransaction((transaction) => {
109
+ return this.save(collection, {
110
+ ...options,
111
+ transaction
112
+ });
113
+ });
114
+ }
115
+
104
116
  if (collection.deleted) {
105
- await this.#model.destroy({id: collection.id});
117
+ await this.#relationModel.query().delete().where('collection_id', collection.id).transacting(options.transaction);
118
+ await this.#model.query().delete().where('id', collection.id).transacting(options.transaction);
106
119
  return;
107
120
  }
121
+
108
122
  const data = {
109
123
  id: collection.id,
110
124
  slug: collection.slug,
@@ -113,7 +127,6 @@ module.exports = class BookshelfCollectionsRepository {
113
127
  filter: collection.filter,
114
128
  type: collection.type,
115
129
  feature_image: collection.featureImage || null,
116
- posts: collection.posts.map(postId => ({id: postId})),
117
130
  created_at: collection.createdAt,
118
131
  updated_at: collection.updatedAt
119
132
  };
@@ -122,7 +135,8 @@ module.exports = class BookshelfCollectionsRepository {
122
135
  {id: data.id},
123
136
  {
124
137
  require: false,
125
- transacting: options.transaction
138
+ transacting: options.transaction,
139
+ withRelated: ['collectionPosts']
126
140
  }
127
141
  );
128
142
 
@@ -130,11 +144,59 @@ module.exports = class BookshelfCollectionsRepository {
130
144
  await this.#model.add(data, {
131
145
  transacting: options.transaction
132
146
  });
147
+ const collectionPostsRelations = collection.posts.map((postId, index) => {
148
+ return {
149
+ id: (new ObjectID).toHexString(),
150
+ sort_order: collection.type === 'manual' ? index : 0,
151
+ collection_id: collection.id,
152
+ post_id: postId
153
+ };
154
+ });
155
+ if (collectionPostsRelations.length > 0) {
156
+ await this.#relationModel.query().insert(collectionPostsRelations).transacting(options.transaction);
157
+ }
133
158
  } else {
134
- return this.#model.edit(data, {
159
+ await this.#model.edit(data, {
135
160
  id: data.id,
136
161
  transacting: options.transaction
137
162
  });
163
+
164
+ const collectionPostsRelations = collection.posts.map((postId, index) => {
165
+ return {
166
+ id: (new ObjectID).toHexString(),
167
+ sort_order: collection.type === 'manual' ? index : 0,
168
+ collection_id: collection.id,
169
+ post_id: postId
170
+ };
171
+ });
172
+
173
+ const collectionPostRelationsToDeleteIds = [];
174
+
175
+ if (collection.type === 'manual') {
176
+ await this.#relationModel.query().delete().where('collection_id', collection.id).transacting(options.transaction);
177
+ } else {
178
+ const existingRelations = existing.toJSON().collectionPosts;
179
+
180
+ for (const existingRelation of existingRelations) {
181
+ const found = collectionPostsRelations.find((thing) => {
182
+ return thing.post_id === existingRelation.post_id;
183
+ });
184
+ if (found) {
185
+ found.id = null;
186
+ } else {
187
+ collectionPostRelationsToDeleteIds.push(existingRelation.id);
188
+ }
189
+ }
190
+ }
191
+
192
+ const missingCollectionPostsRelations = collectionPostsRelations.filter(thing => thing.id !== null);
193
+
194
+ if (missingCollectionPostsRelations.length > 0) {
195
+ await this.#relationModel.query().insert(missingCollectionPostsRelations).transacting(options.transaction);
196
+ }
197
+ if (collectionPostRelationsToDeleteIds.length > 0) {
198
+ await this.#relationModel.query().delete().whereIn('id', collectionPostRelationsToDeleteIds).transacting(options.transaction);
199
+ }
138
200
  }
139
201
  }
140
202
  };
@@ -12,7 +12,7 @@ class CollectionsServiceWrapper {
12
12
  const DomainEvents = require('@tryghost/domain-events');
13
13
  const postsRepository = require('./PostsRepository').getInstance();
14
14
  const models = require('../../models');
15
- const collectionsRepositoryInMemory = new BookshelfCollectionsRepository(models.Collection);
15
+ const collectionsRepositoryInMemory = new BookshelfCollectionsRepository(models.Collection, models.CollectionPost);
16
16
 
17
17
  const collectionsService = new CollectionsService({
18
18
  collectionsRepository: collectionsRepositoryInMemory,
@@ -33,16 +33,16 @@ class CollectionsServiceWrapper {
33
33
  async init() {
34
34
  const config = require('../../../shared/config');
35
35
  const labs = require('../../../shared/labs');
36
+
36
37
  // host setting OR labs "collections" flag has to be enabled to run collections service
37
- if (!config.get('hostSettings:collections:enabled') && !(labs.isSet('collections'))) {
38
- return;
39
- }
38
+ if (config.get('hostSettings:collections:enabled') || labs.isSet('collections')) {
39
+ if (inited) {
40
+ return;
41
+ }
40
42
 
41
- if (inited) {
42
- return;
43
+ inited = true;
44
+ this.api.subscribeToEvents();
43
45
  }
44
- inited = true;
45
- this.api.subscribeToEvents();
46
46
  }
47
47
  }
48
48
 
@@ -5,7 +5,10 @@ const emailSuppressionList = require('../email-suppression-list');
5
5
  const models = require('../../models');
6
6
  const urlUtils = require('../../../shared/url-utils');
7
7
  const spamPrevention = require('../../web/shared/middleware/api/spam-prevention');
8
- const {formattedMemberResponse} = require('./utils');
8
+ const {
9
+ formattedMemberResponse,
10
+ formatNewsletterResponse
11
+ } = require('./utils');
9
12
  const errors = require('@tryghost/errors');
10
13
  const tpl = require('@tryghost/tpl');
11
14
 
@@ -144,6 +147,11 @@ const getMemberNewsletters = async function getMemberNewsletters(req, res) {
144
147
  }
145
148
 
146
149
  const data = _.pick(memberData.toJSON(), 'uuid', 'email', 'name', 'newsletters', 'enable_comment_notifications', 'status');
150
+
151
+ if (data.newsletters) {
152
+ data.newsletters = formatNewsletterResponse(data.newsletters);
153
+ }
154
+
147
155
  return res.json(data);
148
156
  } catch (err) {
149
157
  res.writeHead(400);
@@ -175,6 +183,11 @@ const updateMemberNewsletters = async function updateMemberNewsletters(req, res)
175
183
 
176
184
  const updatedMember = await membersService.api.members.update(data, options);
177
185
  const updatedMemberData = _.pick(updatedMember.toJSON(), ['uuid', 'email', 'name', 'newsletters', 'enable_comment_notifications', 'status']);
186
+
187
+ if (updatedMemberData.newsletters) {
188
+ updatedMemberData.newsletters = formatNewsletterResponse(updatedMemberData.newsletters);
189
+ }
190
+
178
191
  res.json(updatedMemberData);
179
192
  } catch (err) {
180
193
  res.writeHead(400);
@@ -4,6 +4,7 @@ function formatNewsletterResponse(newsletters) {
4
4
  });
5
5
  }
6
6
 
7
+ module.exports.formatNewsletterResponse = formatNewsletterResponse;
7
8
  module.exports.formattedMemberResponse = function formattedMemberResponse(member) {
8
9
  if (!member) {
9
10
  return null;
@@ -4,6 +4,16 @@ class RecommendationServiceWrapper {
4
4
  */
5
5
  repository;
6
6
 
7
+ /**
8
+ * @type {import('@tryghost/recommendations').BookshelfClickEventRepository}
9
+ */
10
+ clickEventRepository;
11
+
12
+ /**
13
+ * @type {import('@tryghost/recommendations').BookshelfSubscribeEventRepository}
14
+ */
15
+ subscribeEventRepository;
16
+
7
17
  /**
8
18
  * @type {import('@tryghost/recommendations').RecommendationController}
9
19
  */
@@ -29,7 +39,8 @@ class RecommendationServiceWrapper {
29
39
  BookshelfRecommendationRepository,
30
40
  RecommendationService,
31
41
  RecommendationController,
32
- WellknownService
42
+ WellknownService,
43
+ BookshelfClickEventRepository
33
44
  } = require('@tryghost/recommendations');
34
45
 
35
46
  const mentions = require('../mentions');
@@ -50,11 +61,21 @@ class RecommendationServiceWrapper {
50
61
  this.repository = new BookshelfRecommendationRepository(models.Recommendation, {
51
62
  sentry
52
63
  });
64
+
65
+ this.clickEventRepository = new BookshelfClickEventRepository(models.RecommendationClickEvent, {
66
+ sentry
67
+ });
68
+ this.subscribeEventRepository = new BookshelfClickEventRepository(models.RecommendationSubscribeEvent, {
69
+ sentry
70
+ });
71
+
53
72
  this.service = new RecommendationService({
54
73
  repository: this.repository,
55
74
  recommendationEnablerService,
56
75
  wellknownService,
57
- mentionSendingService: mentions.sendingService
76
+ mentionSendingService: mentions.sendingService,
77
+ clickEventRepository: this.clickEventRepository,
78
+ subscribeEventRepository: this.subscribeEventRepository
58
79
  });
59
80
  this.controller = new RecommendationController({
60
81
  service: this.service
@@ -66,6 +66,11 @@ class SettingsBREADService {
66
66
  const adminUrl = urlUtils.urlFor('admin', true);
67
67
  const signinURL = new URL(adminUrl);
68
68
  signinURL.hash = `/settings/members/?verifyEmail=${token}`;
69
+ // NOTE: to be removed in future, this is to ensure that the new settings are used when enabled
70
+ if (labsService && labsService.isSet('adminXSettings')) {
71
+ signinURL.hash = `/settings-x/portal/edit?verifyEmail=${token}`;
72
+ }
73
+
69
74
  return signinURL.href;
70
75
  }
71
76
  };
@@ -88,6 +88,20 @@ module.exports = function setupMembersApp() {
88
88
  announcementRouter()
89
89
  );
90
90
 
91
+ // Recommendations
92
+ membersApp.post(
93
+ '/api/recommendations/:id/clicked',
94
+ middleware.loadMemberSession,
95
+ http(api.recommendationsPublic.trackClicked)
96
+ );
97
+
98
+ // Recommendations
99
+ membersApp.post(
100
+ '/api/recommendations/:id/subscribed',
101
+ middleware.loadMemberSession,
102
+ http(api.recommendationsPublic.trackSubscribed)
103
+ );
104
+
91
105
  // Allow external systems to read public settings via the members api
92
106
  // Without CORS issues and without a required integration token
93
107
  // 1. Detect if a site is Running Ghost
@@ -182,7 +182,7 @@
182
182
  },
183
183
  "portal": {
184
184
  "url": "https://cdn.jsdelivr.net/ghost/portal@~{version}/umd/portal.min.js",
185
- "version": "2.35"
185
+ "version": "2.36"
186
186
  },
187
187
  "sodoSearch": {
188
188
  "url": "https://cdn.jsdelivr.net/ghost/sodo-search@~{version}/umd/sodo-search.min.js",
@@ -41,5 +41,6 @@ module.exports = {
41
41
  portal_name: 'portal_name',
42
42
  portal_button: 'portal_button',
43
43
  comments_enabled: 'comments_enabled',
44
- recommendations_enabled: 'recommendations_enabled'
44
+ recommendations_enabled: 'recommendations_enabled',
45
+ outbound_link_tagging: 'outbound_link_tagging'
45
46
  };