ghost 5.61.3 → 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.
- package/components/tryghost-adapter-cache-memory-ttl-5.63.0.tgz +0 -0
- package/components/tryghost-adapter-cache-redis-5.63.0.tgz +0 -0
- package/components/{tryghost-adapter-manager-5.61.3.tgz → tryghost-adapter-manager-5.63.0.tgz} +0 -0
- package/components/tryghost-announcement-bar-settings-5.63.0.tgz +0 -0
- package/components/tryghost-api-framework-5.63.0.tgz +0 -0
- package/components/{tryghost-api-version-compatibility-service-5.61.3.tgz → tryghost-api-version-compatibility-service-5.63.0.tgz} +0 -0
- package/components/{tryghost-audience-feedback-5.61.3.tgz → tryghost-audience-feedback-5.63.0.tgz} +0 -0
- package/components/tryghost-bookshelf-repository-5.63.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.63.0.tgz +0 -0
- package/components/tryghost-collections-5.63.0.tgz +0 -0
- package/components/{tryghost-constants-5.61.3.tgz → tryghost-constants-5.63.0.tgz} +0 -0
- package/components/tryghost-custom-theme-settings-service-5.63.0.tgz +0 -0
- package/components/tryghost-data-generator-5.63.0.tgz +0 -0
- package/components/tryghost-domain-events-5.63.0.tgz +0 -0
- package/components/tryghost-donations-5.63.0.tgz +0 -0
- package/components/tryghost-dynamic-routing-events-5.63.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.63.0.tgz +0 -0
- package/components/{tryghost-email-analytics-service-5.61.3.tgz → tryghost-email-analytics-service-5.63.0.tgz} +0 -0
- package/components/tryghost-email-content-generator-5.63.0.tgz +0 -0
- package/components/tryghost-email-events-5.63.0.tgz +0 -0
- package/components/{tryghost-email-service-5.61.3.tgz → tryghost-email-service-5.63.0.tgz} +0 -0
- package/components/tryghost-email-suppression-list-5.63.0.tgz +0 -0
- package/components/tryghost-event-aware-cache-wrapper-5.63.0.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.63.0.tgz +0 -0
- package/components/{tryghost-external-media-inliner-5.61.3.tgz → tryghost-external-media-inliner-5.63.0.tgz} +0 -0
- package/components/{tryghost-extract-api-key-5.61.3.tgz → tryghost-extract-api-key-5.63.0.tgz} +0 -0
- package/components/{tryghost-html-to-plaintext-5.61.3.tgz → tryghost-html-to-plaintext-5.63.0.tgz} +0 -0
- package/components/tryghost-i18n-5.63.0.tgz +0 -0
- package/components/{tryghost-importer-handler-content-files-5.61.3.tgz → tryghost-importer-handler-content-files-5.63.0.tgz} +0 -0
- package/components/{tryghost-importer-revue-5.61.3.tgz → tryghost-importer-revue-5.63.0.tgz} +0 -0
- package/components/tryghost-in-memory-repository-5.63.0.tgz +0 -0
- package/components/{tryghost-job-manager-5.61.3.tgz → tryghost-job-manager-5.63.0.tgz} +0 -0
- package/components/tryghost-link-redirects-5.63.0.tgz +0 -0
- package/components/tryghost-link-replacer-5.63.0.tgz +0 -0
- package/components/{tryghost-link-tracking-5.61.3.tgz → tryghost-link-tracking-5.63.0.tgz} +0 -0
- package/components/tryghost-magic-link-5.63.0.tgz +0 -0
- package/components/tryghost-mail-events-5.63.0.tgz +0 -0
- package/components/tryghost-mailgun-client-5.63.0.tgz +0 -0
- package/components/{tryghost-member-attribution-5.61.3.tgz → tryghost-member-attribution-5.63.0.tgz} +0 -0
- package/components/tryghost-member-events-5.63.0.tgz +0 -0
- package/components/tryghost-members-api-5.63.0.tgz +0 -0
- package/components/{tryghost-members-csv-5.61.3.tgz → tryghost-members-csv-5.63.0.tgz} +0 -0
- package/components/{tryghost-members-events-service-5.61.3.tgz → tryghost-members-events-service-5.63.0.tgz} +0 -0
- package/components/{tryghost-members-importer-5.61.3.tgz → tryghost-members-importer-5.63.0.tgz} +0 -0
- package/components/tryghost-members-offers-5.63.0.tgz +0 -0
- package/components/tryghost-members-payments-5.63.0.tgz +0 -0
- package/components/tryghost-members-ssr-5.63.0.tgz +0 -0
- package/components/{tryghost-members-stripe-service-5.61.3.tgz → tryghost-members-stripe-service-5.63.0.tgz} +0 -0
- package/components/tryghost-mentions-email-report-5.63.0.tgz +0 -0
- package/components/{tryghost-milestones-5.61.3.tgz → tryghost-milestones-5.63.0.tgz} +0 -0
- package/components/{tryghost-minifier-5.61.3.tgz → tryghost-minifier-5.63.0.tgz} +0 -0
- package/components/tryghost-model-to-domain-event-interceptor-5.63.0.tgz +0 -0
- package/components/{tryghost-mw-api-version-mismatch-5.61.3.tgz → tryghost-mw-api-version-mismatch-5.63.0.tgz} +0 -0
- package/components/tryghost-mw-cache-control-5.63.0.tgz +0 -0
- package/components/{tryghost-mw-error-handler-5.61.3.tgz → tryghost-mw-error-handler-5.63.0.tgz} +0 -0
- package/components/tryghost-mw-session-from-token-5.63.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.63.0.tgz +0 -0
- package/components/tryghost-mw-version-match-5.63.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.63.0.tgz +0 -0
- package/components/tryghost-nql-filter-expansions-5.63.0.tgz +0 -0
- package/components/tryghost-oembed-service-5.63.0.tgz +0 -0
- package/components/{tryghost-package-json-5.61.3.tgz → tryghost-package-json-5.63.0.tgz} +0 -0
- package/components/tryghost-post-events-5.63.0.tgz +0 -0
- package/components/tryghost-post-revisions-5.63.0.tgz +0 -0
- package/components/tryghost-posts-service-5.63.0.tgz +0 -0
- package/components/tryghost-recommendations-5.63.0.tgz +0 -0
- package/components/tryghost-referrers-5.63.0.tgz +0 -0
- package/components/{tryghost-security-5.61.3.tgz → tryghost-security-5.63.0.tgz} +0 -0
- package/components/tryghost-session-service-5.63.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.63.0.tgz +0 -0
- package/components/tryghost-slack-notifications-5.63.0.tgz +0 -0
- package/components/{tryghost-staff-service-5.61.3.tgz → tryghost-staff-service-5.63.0.tgz} +0 -0
- package/components/tryghost-stats-service-5.63.0.tgz +0 -0
- package/components/{tryghost-tiers-5.61.3.tgz → tryghost-tiers-5.63.0.tgz} +0 -0
- package/components/{tryghost-update-check-service-5.61.3.tgz → tryghost-update-check-service-5.63.0.tgz} +0 -0
- package/components/{tryghost-verification-trigger-5.61.3.tgz → tryghost-verification-trigger-5.63.0.tgz} +0 -0
- package/components/tryghost-version-notifications-data-service-5.63.0.tgz +0 -0
- package/components/tryghost-webmentions-5.63.0.tgz +0 -0
- package/core/built/admin/assets/{chunk.143.da84b3b434141aff0f01.js → chunk.143.1c5d21facf1f9b9beef9.js} +6 -5
- package/core/built/admin/assets/{chunk.178.659d369f286d4162b294.js → chunk.178.43b03c9ac011a54262b0.js} +4 -4
- package/core/built/admin/assets/{chunk.208.dbf172ad32f72f21a5dc.js → chunk.237.9b7032162949850f6c76.js} +778 -702
- package/core/built/admin/assets/{ghost-8a4e981c272f793157133814ca7c7e84.css → ghost-33664cad4cd6664a8b5fa56e62c5005f.css} +1 -1
- package/core/built/admin/assets/{ghost-54f84395df6a6fb47b37008ded8eba22.js → ghost-7c3f2de2ec83e591ad9f9db5165b2733.js} +68 -63
- package/core/built/admin/assets/ghost-dark-0452daeaee3a9b16dcd954ea60dad518.css +1 -0
- package/core/built/admin/assets/{vendor-3631184082d609038638c1e169a002e7.js → vendor-f8ce8bd43cf5dad6608f828ab48cee9b.js} +249 -322
- package/core/built/admin/index.html +6 -6
- package/core/frontend/helpers/foreach.js +8 -0
- package/core/frontend/helpers/get.js +3 -0
- package/core/frontend/meta/schema.js +1 -1
- package/core/frontend/services/data/checks.js +6 -0
- package/core/frontend/src/cards/css/collection.css +186 -0
- package/core/frontend/src/cards/css/header_v2.css +19 -23
- package/core/frontend/src/cards/css/signup.css +20 -23
- package/core/server/adapters/storage/LocalStorageBase.js +1 -8
- package/core/server/api/endpoints/recommendations-public.js +44 -5
- package/core/server/api/endpoints/recommendations.js +7 -3
- package/core/server/api/endpoints/utils/serializers/input/pages.js +8 -0
- package/core/server/api/endpoints/utils/serializers/input/posts.js +9 -1
- package/core/server/api/endpoints/utils/serializers/output/members.js +22 -5
- package/core/server/api/endpoints/utils/serializers/output/site.js +2 -0
- package/core/server/data/exporter/table-lists.js +3 -1
- package/core/server/data/migrations/versions/5.63/2023-09-12-11-22-10-add-recommendation-click-events-table.js +8 -0
- package/core/server/data/migrations/versions/5.63/2023-09-12-11-22-11-add-recommendation-subscribe-events-table.js +8 -0
- package/core/server/data/migrations/versions/5.63/2023-09-13-13-03-10-add-ghost-core-content-integration.js +57 -0
- package/core/server/data/migrations/versions/5.63/2023-09-13-13-34-11-add-ghost-core-content-integration-key.js +87 -0
- package/core/server/data/schema/fixtures/fixtures.json +7 -0
- package/core/server/data/schema/schema.js +12 -0
- package/core/server/lib/lexical.js +35 -1
- package/core/server/models/post.js +12 -2
- package/core/server/models/recommendation-click-event.js +22 -0
- package/core/server/models/recommendation-subscribe-event.js +22 -0
- package/core/server/services/collections/BookshelfCollectionsRepository.js +67 -5
- package/core/server/services/collections/service.js +8 -8
- package/core/server/services/members/middleware.js +14 -1
- package/core/server/services/members/utils.js +1 -0
- package/core/server/services/mentions/WebmentionMetadata.js +38 -1
- package/core/server/services/mentions/service.js +2 -1
- package/core/server/services/offers/OfferBookshelfRepository.js +230 -0
- package/core/server/services/offers/service.js +7 -3
- package/core/server/services/public-config/site.js +3 -1
- package/core/server/services/recommendations/RecommendationEnablerService.js +28 -0
- package/core/server/services/recommendations/RecommendationServiceWrapper.js +45 -2
- package/core/server/services/settings/SettingsBREADService.js +6 -1
- package/core/server/services/settings/settings-service.js +1 -0
- package/core/server/services/settings-helpers/SettingsHelpers.js +7 -0
- package/core/server/web/members/app.js +21 -0
- package/core/shared/config/defaults.json +2 -2
- package/core/shared/settings-cache/public.js +3 -1
- package/package.json +161 -160
- package/yarn.lock +492 -512
- package/components/tryghost-adapter-cache-memory-ttl-5.61.3.tgz +0 -0
- package/components/tryghost-adapter-cache-redis-5.61.3.tgz +0 -0
- package/components/tryghost-announcement-bar-settings-5.61.3.tgz +0 -0
- package/components/tryghost-api-framework-5.61.3.tgz +0 -0
- package/components/tryghost-bookshelf-repository-5.61.3.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.61.3.tgz +0 -0
- package/components/tryghost-collections-5.61.3.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.61.3.tgz +0 -0
- package/components/tryghost-data-generator-5.61.3.tgz +0 -0
- package/components/tryghost-domain-events-5.61.3.tgz +0 -0
- package/components/tryghost-donations-5.61.3.tgz +0 -0
- package/components/tryghost-dynamic-routing-events-5.61.3.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.61.3.tgz +0 -0
- package/components/tryghost-email-content-generator-5.61.3.tgz +0 -0
- package/components/tryghost-email-events-5.61.3.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.61.3.tgz +0 -0
- package/components/tryghost-event-aware-cache-wrapper-5.61.3.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.61.3.tgz +0 -0
- package/components/tryghost-i18n-5.61.3.tgz +0 -0
- package/components/tryghost-in-memory-repository-5.61.3.tgz +0 -0
- package/components/tryghost-link-redirects-5.61.3.tgz +0 -0
- package/components/tryghost-link-replacer-5.61.3.tgz +0 -0
- package/components/tryghost-magic-link-5.61.3.tgz +0 -0
- package/components/tryghost-mail-events-5.61.3.tgz +0 -0
- package/components/tryghost-mailgun-client-5.61.3.tgz +0 -0
- package/components/tryghost-member-events-5.61.3.tgz +0 -0
- package/components/tryghost-members-api-5.61.3.tgz +0 -0
- package/components/tryghost-members-offers-5.61.3.tgz +0 -0
- package/components/tryghost-members-payments-5.61.3.tgz +0 -0
- package/components/tryghost-members-ssr-5.61.3.tgz +0 -0
- package/components/tryghost-mentions-email-report-5.61.3.tgz +0 -0
- package/components/tryghost-model-to-domain-event-interceptor-5.61.3.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.61.3.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.61.3.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.61.3.tgz +0 -0
- package/components/tryghost-mw-version-match-5.61.3.tgz +0 -0
- package/components/tryghost-mw-vhost-5.61.3.tgz +0 -0
- package/components/tryghost-nql-filter-expansions-5.61.3.tgz +0 -0
- package/components/tryghost-oembed-service-5.61.3.tgz +0 -0
- package/components/tryghost-post-events-5.61.3.tgz +0 -0
- package/components/tryghost-post-revisions-5.61.3.tgz +0 -0
- package/components/tryghost-posts-service-5.61.3.tgz +0 -0
- package/components/tryghost-recommendations-5.61.3.tgz +0 -0
- package/components/tryghost-referrers-5.61.3.tgz +0 -0
- package/components/tryghost-session-service-5.61.3.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.61.3.tgz +0 -0
- package/components/tryghost-slack-notifications-5.61.3.tgz +0 -0
- package/components/tryghost-stats-service-5.61.3.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.61.3.tgz +0 -0
- package/components/tryghost-webmentions-5.61.3.tgz +0 -0
- package/core/built/admin/assets/ghost-dark-084169b0e968ef763dfbbf63b253e0c6.css +0 -1
- /package/core/built/admin/assets/{chunk.208.dbf172ad32f72f21a5dc.js.LICENSE.txt → chunk.237.9b7032162949850f6c76.js.LICENSE.txt} +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
//@ts-check
|
|
2
|
+
const _ = require('lodash');
|
|
2
3
|
const debug = require('@tryghost/debug')('api:endpoints:utils:serializers:output:members');
|
|
3
4
|
const {unparse} = require('@tryghost/members-csv');
|
|
4
5
|
const mappers = require('./mappers');
|
|
@@ -113,6 +114,26 @@ function serializeAttribution(attribution) {
|
|
|
113
114
|
};
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
function serializeNewsletter(newsletter) {
|
|
118
|
+
const newsletterFields = [
|
|
119
|
+
'id',
|
|
120
|
+
'name',
|
|
121
|
+
'description',
|
|
122
|
+
'status'
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
return _.pick(newsletter, newsletterFields);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function serializeNewsletters(newsletters) {
|
|
129
|
+
return newsletters
|
|
130
|
+
.filter(newsletter => newsletter.status === 'active')
|
|
131
|
+
.sort((a, b) => {
|
|
132
|
+
return a.sort_order - b.sort_order;
|
|
133
|
+
})
|
|
134
|
+
.map(newsletter => serializeNewsletter(newsletter));
|
|
135
|
+
}
|
|
136
|
+
|
|
116
137
|
/**
|
|
117
138
|
* @param {import('bookshelf').Model} member
|
|
118
139
|
* @param {object} options
|
|
@@ -174,11 +195,7 @@ function serializeMember(member, options) {
|
|
|
174
195
|
serialized.email_suppression = json.email_suppression;
|
|
175
196
|
|
|
176
197
|
if (json.newsletters) {
|
|
177
|
-
serialized.newsletters = json.newsletters
|
|
178
|
-
.filter(newsletter => newsletter.status === 'active')
|
|
179
|
-
.sort((a, b) => {
|
|
180
|
-
return a.sort_order - b.sort_order;
|
|
181
|
-
});
|
|
198
|
+
serialized.newsletters = serializeNewsletters(json.newsletters);
|
|
182
199
|
}
|
|
183
200
|
// override the `subscribed` param to mean "subscribed to any active newsletter"
|
|
184
201
|
serialized.subscribed = false;
|
|
@@ -51,7 +51,9 @@ const BACKUP_TABLES = [
|
|
|
51
51
|
'milestones',
|
|
52
52
|
'collections',
|
|
53
53
|
'collections_posts',
|
|
54
|
-
'recommendations'
|
|
54
|
+
'recommendations',
|
|
55
|
+
'recommendation_click_events',
|
|
56
|
+
'recommendation_subscribe_events'
|
|
55
57
|
];
|
|
56
58
|
|
|
57
59
|
// NOTE: exposing only tables which are going to be included in a "default" export file
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const {addTable} = require('../../utils');
|
|
2
|
+
|
|
3
|
+
module.exports = addTable('recommendation_click_events', {
|
|
4
|
+
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
5
|
+
recommendation_id: {type: 'string', maxlength: 24, nullable: false, references: 'recommendations.id', unique: false, cascadeDelete: true},
|
|
6
|
+
member_id: {type: 'string', maxlength: 24, nullable: true, references: 'members.id', unique: false, setNullDelete: true},
|
|
7
|
+
created_at: {type: 'dateTime', nullable: false}
|
|
8
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const {addTable} = require('../../utils');
|
|
2
|
+
|
|
3
|
+
module.exports = addTable('recommendation_subscribe_events', {
|
|
4
|
+
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
5
|
+
recommendation_id: {type: 'string', maxlength: 24, nullable: false, references: 'recommendations.id', unique: false, cascadeDelete: true},
|
|
6
|
+
member_id: {type: 'string', maxlength: 24, nullable: true, references: 'members.id', unique: false, setNullDelete: true},
|
|
7
|
+
created_at: {type: 'dateTime', nullable: false}
|
|
8
|
+
});
|
|
@@ -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
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
|
+
const errors = require('@tryghost/errors');
|
|
2
3
|
const urlUtils = require('../../shared/url-utils');
|
|
3
4
|
const config = require('../../shared/config');
|
|
4
5
|
const storage = require('../adapters/storage');
|
|
@@ -6,6 +7,7 @@ const storage = require('../adapters/storage');
|
|
|
6
7
|
let nodes;
|
|
7
8
|
let lexicalHtmlRenderer;
|
|
8
9
|
let urlTransformMap;
|
|
10
|
+
let postsService;
|
|
9
11
|
|
|
10
12
|
function populateNodes() {
|
|
11
13
|
const {DEFAULT_NODES} = require('@tryghost/kg-default-nodes');
|
|
@@ -27,6 +29,23 @@ module.exports = {
|
|
|
27
29
|
},
|
|
28
30
|
|
|
29
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
|
+
|
|
30
49
|
const options = Object.assign({
|
|
31
50
|
siteUrl: config.get('url'),
|
|
32
51
|
imageOptimization: config.get('imageOptimization'),
|
|
@@ -42,7 +61,8 @@ module.exports = {
|
|
|
42
61
|
createDocument() {
|
|
43
62
|
const {JSDOM} = require('jsdom');
|
|
44
63
|
return (new JSDOM()).window.document;
|
|
45
|
-
}
|
|
64
|
+
},
|
|
65
|
+
getCollectionPosts
|
|
46
66
|
}, userOptions);
|
|
47
67
|
|
|
48
68
|
return await this.lexicalHtmlRenderer.render(lexical, options);
|
|
@@ -78,5 +98,19 @@ module.exports = {
|
|
|
78
98
|
}
|
|
79
99
|
|
|
80
100
|
return urlTransformMap;
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
get htmlToLexicalConverter() {
|
|
104
|
+
try {
|
|
105
|
+
return require('@tryghost/kg-html-to-lexical').htmlToLexical;
|
|
106
|
+
} catch (err) {
|
|
107
|
+
throw new errors.InternalServerError({
|
|
108
|
+
message: 'Unable to convert from source HTML to Lexical',
|
|
109
|
+
context: 'The html-to-lexical package was not installed',
|
|
110
|
+
help: 'Please review any errors from the install process by checking the Ghost logs',
|
|
111
|
+
code: 'HTML_TO_LEXICAL_INSTALLATION',
|
|
112
|
+
err: err
|
|
113
|
+
});
|
|
114
|
+
}
|
|
81
115
|
}
|
|
82
116
|
};
|
|
@@ -552,6 +552,16 @@ Post = ghostBookshelf.Model.extend({
|
|
|
552
552
|
let tagsToSave;
|
|
553
553
|
const ops = [];
|
|
554
554
|
|
|
555
|
+
// normally we don't allow both mobiledoc & lexical through at the API level but there's
|
|
556
|
+
// an exception for ?source=html which always sets both when the lexical editor is enabled.
|
|
557
|
+
// That's necessary because at the input serializer layer we don't have access to the
|
|
558
|
+
// actual model to check if this would result in a change of format
|
|
559
|
+
if (this.previous('mobiledoc') && this.get('lexical')) {
|
|
560
|
+
this.set('lexical', null);
|
|
561
|
+
} else if (this.get('mobiledoc') && this.get('lexical')) {
|
|
562
|
+
this.set('mobiledoc', null);
|
|
563
|
+
}
|
|
564
|
+
|
|
555
565
|
// CASE: disallow published -> scheduled
|
|
556
566
|
// @TODO: remove when we have versioning based on updated_at
|
|
557
567
|
if (newStatus !== olderStatus && newStatus === 'scheduled' && olderStatus === 'published') {
|
|
@@ -653,7 +663,7 @@ Post = ghostBookshelf.Model.extend({
|
|
|
653
663
|
|
|
654
664
|
// If we're force re-rendering we want to make sure that all image cards
|
|
655
665
|
// have original dimensions stored in the payload for use by card renderers
|
|
656
|
-
if (options.force_rerender) {
|
|
666
|
+
if (options.force_rerender && this.get('mobiledoc')) {
|
|
657
667
|
this.set('mobiledoc', await mobiledocLib.populateImageSizes(this.get('mobiledoc')));
|
|
658
668
|
}
|
|
659
669
|
|
|
@@ -690,7 +700,7 @@ Post = ghostBookshelf.Model.extend({
|
|
|
690
700
|
)
|
|
691
701
|
) {
|
|
692
702
|
try {
|
|
693
|
-
this.set('html', await lexicalLib.render(this.get('lexical')));
|
|
703
|
+
this.set('html', await lexicalLib.render(this.get('lexical'), {transacting: options.transacting}));
|
|
694
704
|
} catch (err) {
|
|
695
705
|
throw new errors.ValidationError({
|
|
696
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
|
-
|
|
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.#
|
|
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
|
-
|
|
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 (
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
if (config.get('hostSettings:collections:enabled') || labs.isSet('collections')) {
|
|
39
|
+
if (inited) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
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 {
|
|
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);
|