ghost 4.22.3 → 4.25.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/.c8rc.json +24 -0
- package/.eslintrc.js +39 -0
- package/Gruntfile.js +0 -1
- package/content/public/README.md +3 -0
- package/content/themes/casper/assets/built/casper.js +1 -1
- package/content/themes/casper/assets/built/casper.js.map +1 -1
- package/content/themes/casper/assets/built/global.css +1 -1
- package/content/themes/casper/assets/built/global.css.map +1 -1
- package/content/themes/casper/assets/built/screen.css +1 -1
- package/content/themes/casper/assets/built/screen.css.map +1 -1
- package/content/themes/casper/assets/css/global.css +6 -1
- package/content/themes/casper/assets/css/screen.css +32 -216
- package/content/themes/casper/default.hbs +2 -2
- package/content/themes/casper/package.json +3 -2
- package/content/themes/casper/post.hbs +1 -1
- package/content/themes/casper/yarn.lock +173 -123
- package/core/app.js +12 -1
- package/core/boot.js +47 -28
- package/core/bridge.js +10 -10
- package/core/built/assets/{chunk.3.324fd0cc598c73650219.js → chunk.3.8f95b516d88ff4eec64c.js} +18 -18
- package/core/built/assets/ghost-dark-d690e732e17ffc794e2e59c1467ca282.css +1 -0
- package/core/built/assets/ghost.min-043bb7480a0810109b130f13b2a4235e.css +1 -0
- package/core/built/assets/{ghost.min-7da921f6c6cac3fe10da1ba104575440.js → ghost.min-bc72f685c1c9adc9885925c1412435a5.js} +563 -605
- package/core/built/assets/icons/audio-upload.svg +8 -0
- package/core/built/assets/icons/powered-by-tenor.svg +35 -0
- package/core/built/assets/icons/tenor.svg +7 -0
- package/core/built/assets/{vendor.min-413f887176a041e6dbf88214ca9a7481.js → vendor.min-d1234c632a54502777c34e50752fa3fc.js} +4622 -3631
- package/core/frontend/apps/amp/lib/helpers/amp_content.js +2 -2
- package/core/frontend/apps/amp/lib/views/amp.hbs +112 -0
- package/core/frontend/apps/private-blogging/index.js +1 -1
- package/core/frontend/apps/private-blogging/lib/router.js +1 -1
- package/core/frontend/services/apps/index.js +1 -1
- package/core/frontend/services/apps/loader.js +3 -3
- package/core/frontend/services/card-assets/index.js +0 -12
- package/core/frontend/services/card-assets/service.js +29 -28
- package/core/frontend/services/helpers/handlebars.js +1 -1
- package/core/frontend/services/routing/CollectionRouter.js +4 -5
- package/core/frontend/services/routing/EmailRouter.js +1 -1
- package/core/frontend/services/routing/ParentRouter.js +0 -8
- package/core/frontend/services/routing/PreviewRouter.js +1 -1
- package/core/frontend/services/routing/StaticPagesRouter.js +1 -1
- package/core/frontend/services/routing/StaticRoutesRouter.js +4 -4
- package/core/frontend/services/routing/TaxonomyRouter.js +3 -3
- package/core/frontend/services/routing/{middlewares → middleware}/index.js +0 -0
- package/core/frontend/services/routing/{middlewares → middleware}/page-param.js +0 -0
- package/core/frontend/services/routing/router-manager.js +7 -2
- package/core/frontend/services/rss/generate-feed.js +2 -1
- package/core/frontend/services/theme-engine/middleware/ensure-active-theme.js +34 -0
- package/core/frontend/services/theme-engine/middleware/index.js +6 -0
- package/core/frontend/services/theme-engine/middleware/update-global-template-options.js +116 -0
- package/core/frontend/services/theme-engine/middleware/update-local-template-data.js +9 -0
- package/core/frontend/services/theme-engine/middleware/update-local-template-options.js +57 -0
- package/core/frontend/src/cards/css/bookmark.css +72 -47
- package/core/frontend/src/cards/css/button.css +4 -0
- package/core/frontend/src/cards/css/callout.css +40 -3
- package/core/frontend/src/cards/css/gallery.css +15 -10
- package/core/frontend/src/cards/css/nft.css +20 -11
- package/core/frontend/src/cards/css/toggle.css +58 -0
- package/core/frontend/src/cards/js/toggle.js +16 -0
- package/core/frontend/web/middleware/error-handler.js +93 -0
- package/core/frontend/web/middleware/handle-image-sizes.js +3 -6
- package/core/frontend/web/middleware/index.js +1 -0
- package/core/frontend/web/middleware/serve-public-file.js +39 -16
- package/core/frontend/web/site.js +11 -14
- package/core/server/adapters/scheduling/SchedulingDefault.js +2 -2
- package/core/server/adapters/storage/LocalStorageBase.js +2 -2
- package/core/server/api/canary/authentication.js +1 -1
- package/core/server/api/canary/db.js +2 -2
- package/core/server/api/canary/media.js +3 -2
- package/core/server/api/canary/oembed.js +16 -1
- package/core/server/api/canary/session.js +1 -1
- package/core/server/api/canary/slugs.js +1 -1
- package/core/server/api/canary/utils/permissions.js +2 -2
- package/core/server/api/canary/utils/serializers/output/config.js +2 -6
- package/core/server/api/v2/authentication.js +1 -1
- package/core/server/api/v2/db.js +2 -2
- package/core/server/api/v2/session.js +1 -1
- package/core/server/api/v2/slugs.js +1 -1
- package/core/server/api/v2/utils/permissions.js +2 -2
- package/core/server/api/v3/authentication.js +1 -1
- package/core/server/api/v3/db.js +2 -2
- package/core/server/api/v3/session.js +1 -1
- package/core/server/api/v3/slugs.js +1 -1
- package/core/server/api/v3/utils/permissions.js +2 -2
- package/core/server/data/db/connection.js +7 -0
- package/core/server/data/db/state-manager.js +4 -4
- package/core/server/data/exporter/export-filename.js +1 -1
- package/core/server/data/importer/handlers/json.js +1 -1
- package/core/server/data/importer/import-manager.js +1 -1
- package/core/server/data/importer/importers/data/base.js +1 -1
- package/core/server/data/importer/importers/data/data-importer.js +3 -3
- package/core/server/data/migrations/init/2-create-fixtures.js +3 -20
- package/core/server/data/migrations/utils.js +2 -2
- package/core/server/data/migrations/versions/1.21/1-add-contributor-role.js +5 -5
- package/core/server/data/migrations/versions/1.25/1-update-koenig-beta-html.js +1 -0
- package/core/server/data/migrations/versions/2.15/2-insert-zapier-integration.js +3 -3
- package/core/server/data/migrations/versions/2.2/3-insert-admin-integration-role.js +5 -5
- package/core/server/data/migrations/versions/2.27/1-insert-ghost-db-backup-role.js +5 -6
- package/core/server/data/migrations/versions/2.27/2-insert-db-backup-integration.js +3 -4
- package/core/server/data/migrations/versions/2.28/3-insert-ghost-scheduler-role.js +7 -7
- package/core/server/data/migrations/versions/2.28/4-insert-scheduler-integration.js +3 -3
- package/core/server/data/migrations/versions/3.1/08-add-uuid-values-to-members.js +1 -0
- package/core/server/data/migrations/versions/3.22/02-settings-key-renames.js +2 -0
- package/core/server/data/migrations/versions/3.22/05-migrate-members-subscription-settings.js +3 -0
- package/core/server/data/migrations/versions/3.22/06-migrate-stripe-connect-settings.js +2 -0
- package/core/server/data/migrations/versions/3.23/01-migrate-bulk-email-settings.js +1 -0
- package/core/server/data/migrations/versions/3.29/01-remove-duplicate-subscriptions.js +2 -0
- package/core/server/data/migrations/versions/3.29/02-remove-duplicate-customers.js +2 -0
- package/core/server/data/migrations/versions/3.38/04-populate-recipient-filter-column.js +2 -0
- package/core/server/data/migrations/versions/4.0/01-update-mobiledoc.js +2 -0
- package/core/server/data/migrations/versions/4.0/03-populate-status-column-for-members.js +4 -0
- package/core/server/data/migrations/versions/4.0/06-populate-members-subscribe-events-table.js +1 -0
- package/core/server/data/migrations/versions/4.0/17-populate-members-status-events-table.js +1 -0
- package/core/server/data/migrations/versions/4.0/18-transform-urls-absolute-to-transform-ready.js +5 -0
- package/core/server/data/migrations/versions/4.0/22-solve-orphaned-webhooks.js +1 -0
- package/core/server/data/migrations/versions/4.0/23-regenerate-posts-html.js +1 -0
- package/core/server/data/migrations/versions/4.0/25-populate-members-paid-subscription-events-table.js +2 -1
- package/core/server/data/migrations/versions/4.12/02-fix-member-statuses.js +1 -0
- package/core/server/data/migrations/versions/4.14/01-fix-comped-member-statuses.js +3 -0
- package/core/server/data/migrations/versions/4.14/02-fix-free-members-status-events.js +1 -0
- package/core/server/data/migrations/versions/4.20/05-remove-not-null-constraint-from-portal-title.js +2 -0
- package/core/server/data/migrations/versions/4.23/01-truncate-offer-names.js +59 -0
- package/core/server/data/migrations/versions/4.3/04-attach-members-to-product.js +1 -0
- package/core/server/data/migrations/versions/4.4/01-restore-free-members-signup-setting-from-backup.js +1 -0
- package/core/server/data/migrations/versions/4.6/01-remove-comped-status.js +1 -0
- package/core/server/data/migrations/versions/4.8/04-migrate-show-newsletter-header-setting.js +1 -0
- package/core/server/data/migrations/versions/4.9/05-fix-missed-mobiledoc-url-transforms.js +1 -0
- package/core/server/data/migrations/versions/4.9/06-add-comped-status.js +1 -0
- package/core/server/data/migrations/versions/4.9/07-update-comped-members-status-events.js +1 -0
- package/core/server/data/schema/commands.js +2 -2
- package/core/server/data/schema/fixtures/fixture-manager.js +340 -0
- package/core/server/data/schema/fixtures/index.js +8 -2
- package/core/server/ghost-server.js +2 -2
- package/core/server/lib/image/image-size.js +2 -2
- package/core/server/models/base/listeners.js +2 -2
- package/core/server/models/member-email-change-event.js +2 -2
- package/core/server/models/member-login-event.js +2 -2
- package/core/server/models/member-paid-subscription-event.js +3 -3
- package/core/server/models/member-payment-event.js +3 -3
- package/core/server/models/member-product-event.js +6 -6
- package/core/server/models/member-status-event.js +5 -3
- package/core/server/models/member-subscribe-event.js +9 -3
- package/core/server/models/relations/authors.js +1 -1
- package/core/server/models/settings.js +1 -1
- package/core/server/services/auth/passwordreset.js +1 -1
- package/core/server/services/auth/setup.js +1 -1
- package/core/server/services/email-analytics/jobs/index.js +1 -1
- package/core/server/services/mega/mega.js +6 -4
- package/core/server/services/mega/post-email-serializer.js +5 -1
- package/core/server/services/mega/segment-parser.js +1 -2
- package/core/server/services/mega/template.js +52 -37
- package/core/server/services/members/api.js +22 -0
- package/core/server/services/members/config.js +1 -1
- package/core/server/services/members/emails/signup-paid.js +168 -0
- package/core/server/services/members/service.js +6 -2
- package/core/server/services/members/stripe-connect.js +4 -2
- package/core/server/services/nft-oembed.js +13 -22
- package/core/server/services/oembed.js +28 -24
- package/core/server/services/permissions/can-this.js +1 -1
- package/core/server/services/public-config/config.js +1 -1
- package/core/server/services/redirects/api.js +20 -25
- package/core/server/services/redirects/index.js +18 -10
- package/core/server/services/redirects/utils.js +14 -0
- package/core/server/services/redirects/validation.js +10 -0
- package/core/server/services/route-settings/default-settings-manager.js +1 -1
- package/core/server/services/route-settings/index.js +40 -17
- package/core/server/services/route-settings/route-settings.js +120 -115
- package/core/server/services/route-settings/settings-loader.js +18 -36
- package/core/server/services/route-settings/yaml-parser.js +1 -1
- package/core/server/services/slack.js +1 -1
- package/core/server/services/themes/activation-bridge.js +3 -3
- package/core/server/services/themes/storage.js +2 -2
- package/core/server/services/twitter-embed.js +80 -0
- package/core/server/services/url/LocalFileCache.js +75 -0
- package/core/server/services/url/Resources.js +8 -2
- package/core/server/services/url/UrlGenerator.js +23 -20
- package/core/server/services/url/UrlService.js +75 -63
- package/core/server/services/url/index.js +17 -3
- package/core/server/services/xmlrpc.js +2 -2
- package/core/server/web/admin/app.js +7 -10
- package/core/server/web/admin/controller.js +35 -12
- package/core/server/web/admin/middleware/redirect-admin-urls.js +15 -0
- package/core/server/web/admin/views/default-prod.html +4 -4
- package/core/server/web/admin/views/default.html +4 -4
- package/core/server/web/api/app.js +1 -1
- package/core/server/web/api/canary/admin/app.js +3 -6
- package/core/server/web/api/canary/admin/middleware.js +7 -7
- package/core/server/web/api/canary/admin/routes.js +5 -5
- package/core/server/web/api/canary/content/app.js +3 -6
- package/core/server/web/api/canary/content/middleware.js +3 -3
- package/core/server/web/api/v2/admin/app.js +3 -6
- package/core/server/web/api/v2/admin/middleware.js +7 -7
- package/core/server/web/api/v2/admin/routes.js +5 -5
- package/core/server/web/api/v2/content/app.js +3 -6
- package/core/server/web/api/v2/content/middleware.js +3 -3
- package/core/server/web/api/v3/admin/app.js +3 -6
- package/core/server/web/api/v3/admin/middleware.js +7 -7
- package/core/server/web/api/v3/admin/routes.js +5 -5
- package/core/server/web/api/v3/content/app.js +3 -6
- package/core/server/web/api/v3/content/middleware.js +3 -3
- package/core/server/web/members/app.js +6 -9
- package/core/server/web/oauth/app.js +0 -4
- package/core/server/web/parent/app.js +17 -9
- package/core/server/web/parent/frontend.js +1 -1
- package/core/server/web/shared/index.js +2 -2
- package/core/server/web/shared/{middlewares → middleware}/api/index.js +0 -0
- package/core/server/web/shared/{middlewares → middleware}/api/spam-prevention.js +0 -0
- package/core/server/web/shared/{middlewares → middleware}/brute.js +0 -0
- package/core/server/web/shared/{middlewares → middleware}/cache-control.js +0 -0
- package/core/server/web/shared/middleware/error-handler.js +224 -0
- package/core/server/web/shared/{middlewares → middleware}/index.js +0 -4
- package/core/server/web/shared/{middlewares → middleware}/pretty-urls.js +0 -0
- package/core/server/web/shared/{middlewares → middleware}/uncapitalise.js +0 -0
- package/core/server/web/shared/{middlewares → middleware}/url-redirects.js +0 -0
- package/core/shared/config/defaults.json +13 -1
- package/core/shared/config/helpers.js +42 -0
- package/core/shared/config/loader.js +1 -1
- package/core/shared/labs.js +9 -5
- package/core/shared/sentry.js +1 -1
- package/loggingrc.js +19 -20
- package/package.json +38 -37
- package/yarn.lock +1064 -892
- package/content/themes/casper/assets/js/gallery-card.js +0 -24
- package/core/built/assets/ghost-dark-39fb496d051565531062d7e047d1c0b1.css +0 -1
- package/core/built/assets/ghost.min-4207edfc1ae0a3f9f6505ca00d20b0c0.css +0 -1
- package/core/frontend/services/theme-engine/middleware.js +0 -209
- package/core/server/data/schema/fixtures/utils.js +0 -321
- package/core/server/web/parent/vhost-utils.js +0 -39
- package/core/server/web/shared/middlewares/error-handler.js +0 -329
- package/core/server/web/shared/middlewares/maintenance.js +0 -25
|
@@ -3,6 +3,7 @@ const {createTransactionalMigration} = require('../../utils.js');
|
|
|
3
3
|
|
|
4
4
|
module.exports = createTransactionalMigration(
|
|
5
5
|
async function up(knex) {
|
|
6
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
6
7
|
const compedMemberIds = (await knex('members')
|
|
7
8
|
.select('members.id')
|
|
8
9
|
.innerJoin(
|
|
@@ -15,6 +15,7 @@ module.exports = createTransactionalMigration(
|
|
|
15
15
|
logging.info(`Found ${compedMembers.length} comped members - checking members_status_events`);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
18
19
|
for (const member of compedMembers) {
|
|
19
20
|
const mostRecentStatusEvent = await knex('members_status_events')
|
|
20
21
|
.select('*')
|
|
@@ -139,7 +139,7 @@ async function hasForeignSQLite({fromTable, fromColumn, toTable, toColumn, trans
|
|
|
139
139
|
const client = knex.client.config.client;
|
|
140
140
|
|
|
141
141
|
if (client !== 'sqlite3') {
|
|
142
|
-
throw new errors.
|
|
142
|
+
throw new errors.InternalServerError({
|
|
143
143
|
message: tpl(messages.hasForeignSQLite3)
|
|
144
144
|
});
|
|
145
145
|
}
|
|
@@ -265,7 +265,7 @@ async function hasPrimaryKeySQLite(tableName, transaction) {
|
|
|
265
265
|
const client = knex.client.config.client;
|
|
266
266
|
|
|
267
267
|
if (client !== 'sqlite3') {
|
|
268
|
-
throw new errors.
|
|
268
|
+
throw new errors.InternalServerError({
|
|
269
269
|
message: tpl(messages.hasPrimaryKeySQLiteError)
|
|
270
270
|
});
|
|
271
271
|
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
const _ = require('lodash');
|
|
2
|
+
const Promise = require('bluebird');
|
|
3
|
+
const logging = require('@tryghost/logging');
|
|
4
|
+
const {sequence} = require('@tryghost/promise');
|
|
5
|
+
|
|
6
|
+
const models = require('../../../models');
|
|
7
|
+
const baseUtils = require('../../../models/base/utils');
|
|
8
|
+
|
|
9
|
+
const moment = require('moment');
|
|
10
|
+
|
|
11
|
+
class FixtureManager {
|
|
12
|
+
constructor(fixtures) {
|
|
13
|
+
this.fixtures = fixtures;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* ### Match Func
|
|
18
|
+
* Figures out how to match across various combinations of keys and values.
|
|
19
|
+
* Match can be a string or an array containing 2 strings
|
|
20
|
+
* Key and Value are the values to be found
|
|
21
|
+
* Value can also be an array, in which case we look for a match in the array.
|
|
22
|
+
* @api private
|
|
23
|
+
* @param {String|Array} match
|
|
24
|
+
* @param {String|Integer} key
|
|
25
|
+
* @param {String|Array} [value]
|
|
26
|
+
* @returns {Function} matching function
|
|
27
|
+
*/
|
|
28
|
+
static matchFunc(match, key, value) {
|
|
29
|
+
if (_.isArray(match)) {
|
|
30
|
+
return function (item) {
|
|
31
|
+
let valueTest = true;
|
|
32
|
+
|
|
33
|
+
if (_.isArray(value)) {
|
|
34
|
+
valueTest = value.indexOf(item.get(match[1])) > -1;
|
|
35
|
+
} else if (value !== 'all') {
|
|
36
|
+
valueTest = item.get(match[1]) === value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return item.get(match[0]) === key && valueTest;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return function (item) {
|
|
44
|
+
key = key === 0 && value ? value : key;
|
|
45
|
+
return item.get(match) === key;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static matchObj(match, item) {
|
|
50
|
+
const matchedObj = {};
|
|
51
|
+
|
|
52
|
+
if (_.isArray(match)) {
|
|
53
|
+
_.each(match, (matchProp) => {
|
|
54
|
+
matchedObj[matchProp] = item.get(matchProp);
|
|
55
|
+
});
|
|
56
|
+
} else {
|
|
57
|
+
matchedObj[match] = item.get(match);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return matchedObj;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Add All Fixtures
|
|
65
|
+
*
|
|
66
|
+
* Helper method to handle adding all fixtures
|
|
67
|
+
*
|
|
68
|
+
* @param {object} options
|
|
69
|
+
* @returns
|
|
70
|
+
*/
|
|
71
|
+
async addAllFixtures(options) {
|
|
72
|
+
const localOptions = _.merge({
|
|
73
|
+
context: {internal: true},
|
|
74
|
+
migrating: true
|
|
75
|
+
}, options);
|
|
76
|
+
|
|
77
|
+
await Promise.mapSeries(this.fixtures.models, (model) => {
|
|
78
|
+
logging.info('Model: ' + model.name);
|
|
79
|
+
|
|
80
|
+
return this.addFixturesForModel(model, localOptions);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await Promise.mapSeries(this.fixtures.relations, (relation) => {
|
|
84
|
+
logging.info('Relation: ' + relation.from.model + ' to ' + relation.to.model);
|
|
85
|
+
return this.addFixturesForRelation(relation, localOptions);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/*
|
|
90
|
+
* Find methods - use the local fixtures
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* ### Find Model Fixture
|
|
95
|
+
* Finds a model fixture based on model name
|
|
96
|
+
* @api private
|
|
97
|
+
* @param {String} modelName
|
|
98
|
+
* @returns {Object} model fixture
|
|
99
|
+
*/
|
|
100
|
+
findModelFixture(modelName) {
|
|
101
|
+
return _.find(this.fixtures.models, (modelFixture) => {
|
|
102
|
+
return modelFixture.name === modelName;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* ### Find Model Fixture Entry
|
|
108
|
+
* Find a single model fixture entry by model name & a matching expression for the FIND function
|
|
109
|
+
* @param {String} modelName
|
|
110
|
+
* @param {String|Object|Function} matchExpr
|
|
111
|
+
* @returns {Object} model fixture entry
|
|
112
|
+
*/
|
|
113
|
+
findModelFixtureEntry(modelName, matchExpr) {
|
|
114
|
+
return _.find(this.findModelFixture(modelName).entries, matchExpr);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* ### Find Model Fixtures
|
|
119
|
+
* Find a model fixture name & a matching expression for the FILTER function
|
|
120
|
+
* @param {String} modelName
|
|
121
|
+
* @param {String|Object|Function} matchExpr
|
|
122
|
+
* @returns {Object} model fixture
|
|
123
|
+
*/
|
|
124
|
+
findModelFixtures(modelName, matchExpr) {
|
|
125
|
+
const foundModel = _.cloneDeep(this.findModelFixture(modelName));
|
|
126
|
+
foundModel.entries = _.filter(foundModel.entries, matchExpr);
|
|
127
|
+
return foundModel;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* ### Find Relation Fixture
|
|
132
|
+
* Find a relation fixture by from & to models
|
|
133
|
+
* @api private
|
|
134
|
+
* @param {String} from
|
|
135
|
+
* @param {String} to
|
|
136
|
+
* @returns {Object} relation fixture
|
|
137
|
+
*/
|
|
138
|
+
findRelationFixture(from, to) {
|
|
139
|
+
return _.find(this.fixtures.relations, (relation) => {
|
|
140
|
+
return relation.from.model === from && relation.to.model === to;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* ### Find Permission Relations For Object
|
|
146
|
+
* Specialist function can return the permission relation fixture with only entries for a particular object.model
|
|
147
|
+
* @param {String} objName
|
|
148
|
+
* @returns {Object} fixture relation
|
|
149
|
+
*/
|
|
150
|
+
findPermissionRelationsForObject(objName, role) {
|
|
151
|
+
// Make a copy and delete any entries we don't want
|
|
152
|
+
const foundRelation = _.cloneDeep(this.findRelationFixture('Role', 'Permission'));
|
|
153
|
+
|
|
154
|
+
_.each(foundRelation.entries, (entry, key) => {
|
|
155
|
+
_.each(entry, (perm, obj) => {
|
|
156
|
+
if (obj !== objName) {
|
|
157
|
+
delete entry[obj];
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (_.isEmpty(entry) || (role && role !== key)) {
|
|
162
|
+
delete foundRelation.entries[key];
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return foundRelation;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/******************************************************
|
|
170
|
+
* From here down, the methods require access to models
|
|
171
|
+
* But aren't dependent on this.fixtures
|
|
172
|
+
******************************************************/
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* ### Fetch Relation Data
|
|
176
|
+
* Before we build relations we need to fetch all of the models from both sides so that we can
|
|
177
|
+
* use filter and find to quickly locate the correct models.
|
|
178
|
+
* @api private
|
|
179
|
+
* @param {{from, to, entries}} relation
|
|
180
|
+
* @returns {Promise<*>}
|
|
181
|
+
*/
|
|
182
|
+
fetchRelationData(relation, options) {
|
|
183
|
+
const fromOptions = _.extend({}, options, {withRelated: [relation.from.relation]});
|
|
184
|
+
|
|
185
|
+
const props = {
|
|
186
|
+
from: models[relation.from.model].findAll(fromOptions),
|
|
187
|
+
to: models[relation.to.model].findAll(options)
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return Promise.props(props);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* ### Add Fixtures for Model
|
|
195
|
+
* Takes a model fixture, with a name and some entries and processes these
|
|
196
|
+
* into a sequence of promises to get each fixture added.
|
|
197
|
+
*
|
|
198
|
+
* @param {{name, entries}} modelFixture
|
|
199
|
+
* @returns {Promise<any>}
|
|
200
|
+
*/
|
|
201
|
+
async addFixturesForModel(modelFixture, options = {}) {
|
|
202
|
+
// Clone the fixtures as they get changed in this function.
|
|
203
|
+
// The initial blog posts will be added a `published_at` property, which
|
|
204
|
+
// would change the fixturesHash.
|
|
205
|
+
modelFixture = _.cloneDeep(modelFixture);
|
|
206
|
+
// The Post model fixtures need a `published_at` date, where at least the seconds
|
|
207
|
+
// are different, otherwise `prev_post` and `next_post` helpers won't workd with
|
|
208
|
+
// them.
|
|
209
|
+
if (modelFixture.name === 'Post') {
|
|
210
|
+
_.forEach(modelFixture.entries, (post, index) => {
|
|
211
|
+
if (!post.published_at) {
|
|
212
|
+
post.published_at = moment().add(index, 'seconds');
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const results = await Promise.mapSeries(modelFixture.entries, async (entry) => {
|
|
218
|
+
let data = {};
|
|
219
|
+
|
|
220
|
+
// CASE: if id is specified, only query by id
|
|
221
|
+
if (entry.id) {
|
|
222
|
+
data.id = entry.id;
|
|
223
|
+
} else if (entry.slug) {
|
|
224
|
+
data.slug = entry.slug;
|
|
225
|
+
} else {
|
|
226
|
+
data = _.cloneDeep(entry);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (modelFixture.name === 'Post') {
|
|
230
|
+
data.status = 'all';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const found = await models[modelFixture.name].findOne(data, options);
|
|
234
|
+
if (!found) {
|
|
235
|
+
return models[modelFixture.name].add(entry, options);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
return {expected: modelFixture.entries.length, done: _.compact(results).length};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* ## Add Fixtures for Relation
|
|
244
|
+
* Takes a relation fixtures object, with a from, to and some entries and processes these
|
|
245
|
+
* into a sequence of promises, to get each fixture added.
|
|
246
|
+
*
|
|
247
|
+
* @param {{from, to, entries}} relationFixture
|
|
248
|
+
* @returns {Promise<any>}
|
|
249
|
+
*/
|
|
250
|
+
async addFixturesForRelation(relationFixture, options) {
|
|
251
|
+
const ops = [];
|
|
252
|
+
let max = 0;
|
|
253
|
+
|
|
254
|
+
const data = await this.fetchRelationData(relationFixture, options);
|
|
255
|
+
|
|
256
|
+
_.each(relationFixture.entries, (entry, key) => {
|
|
257
|
+
const fromItem = data.from.find(FixtureManager.matchFunc(relationFixture.from.match, key));
|
|
258
|
+
|
|
259
|
+
// CASE: You add new fixtures e.g. a new role in a new release.
|
|
260
|
+
// As soon as an **older** migration script wants to add permissions for any resource, it iterates over the
|
|
261
|
+
// permissions for each role. But if the role does not exist yet, it won't find the matching db entry and breaks.
|
|
262
|
+
if (!fromItem) {
|
|
263
|
+
logging.warn('Skip: Target database entry not found for key: ' + key);
|
|
264
|
+
return Promise.resolve();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
_.each(entry, (value, entryKey) => {
|
|
268
|
+
let toItems = data.to.filter(FixtureManager.matchFunc(relationFixture.to.match, entryKey, value));
|
|
269
|
+
max += toItems.length;
|
|
270
|
+
|
|
271
|
+
// Remove any duplicates that already exist in the collection
|
|
272
|
+
toItems = _.reject(toItems, (item) => {
|
|
273
|
+
return fromItem
|
|
274
|
+
.related(relationFixture.from.relation)
|
|
275
|
+
.find((model) => {
|
|
276
|
+
const objectToMatch = FixtureManager.matchObj(relationFixture.to.match, item);
|
|
277
|
+
return Object.keys(objectToMatch).every((keyToCheck) => {
|
|
278
|
+
return model.get(keyToCheck) === objectToMatch[keyToCheck];
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
if (toItems && toItems.length > 0) {
|
|
284
|
+
ops.push(function addRelationItems() {
|
|
285
|
+
return baseUtils.attach(
|
|
286
|
+
models[relationFixture.from.Model || relationFixture.from.model],
|
|
287
|
+
fromItem.id,
|
|
288
|
+
relationFixture.from.relation,
|
|
289
|
+
toItems,
|
|
290
|
+
options
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const result = await sequence(ops);
|
|
298
|
+
return {expected: max, done: _(result).map('length').sum()};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async removeFixturesForModel(modelFixture, options) {
|
|
302
|
+
const results = await Promise.mapSeries(modelFixture.entries, async (entry) => {
|
|
303
|
+
const found = models[modelFixture.name].findOne(entry.id ? {id: entry.id} : entry, options);
|
|
304
|
+
if (found) {
|
|
305
|
+
return models[modelFixture.name].destroy(_.extend(options, {id: found.id}));
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
return {expected: modelFixture.entries.length, done: results.length};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async removeFixturesForRelation(relationFixture, options) {
|
|
313
|
+
const data = await this.fetchRelationData(relationFixture, options);
|
|
314
|
+
const ops = [];
|
|
315
|
+
|
|
316
|
+
_.each(relationFixture.entries, (entry, key) => {
|
|
317
|
+
const fromItem = data.from.find(FixtureManager.matchFunc(relationFixture.from.match, key));
|
|
318
|
+
|
|
319
|
+
_.each(entry, (value, entryKey) => {
|
|
320
|
+
const toItems = data.to.filter(FixtureManager.matchFunc(relationFixture.to.match, entryKey, value));
|
|
321
|
+
|
|
322
|
+
if (toItems && toItems.length > 0) {
|
|
323
|
+
ops.push(function detachRelation() {
|
|
324
|
+
return baseUtils.detach(
|
|
325
|
+
models[relationFixture.from.Model || relationFixture.from.model],
|
|
326
|
+
fromItem.id,
|
|
327
|
+
relationFixture.from.relation,
|
|
328
|
+
toItems,
|
|
329
|
+
options
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
return await sequence(ops);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
module.exports = FixtureManager;
|
|
@@ -1,2 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
const FixtureManager = require('./fixture-manager');
|
|
2
|
+
const config = require('../../../../shared/config');
|
|
3
|
+
|
|
4
|
+
const fixturePath = config.get('paths').fixtures;
|
|
5
|
+
const fixtures = require(fixturePath);
|
|
6
|
+
|
|
7
|
+
module.exports.FixtureManager = FixtureManager;
|
|
8
|
+
module.exports.fixtureManager = new FixtureManager(fixtures);
|
|
@@ -107,13 +107,13 @@ class GhostServer {
|
|
|
107
107
|
let ghostError;
|
|
108
108
|
|
|
109
109
|
if (error.code === 'EADDRINUSE') {
|
|
110
|
-
ghostError = new errors.
|
|
110
|
+
ghostError = new errors.InternalServerError({
|
|
111
111
|
message: tpl(messages.addressInUse.error),
|
|
112
112
|
context: tpl(messages.addressInUse.context, {port: config.get('server').port}),
|
|
113
113
|
help: tpl(messages.addressInUse.help)
|
|
114
114
|
});
|
|
115
115
|
} else {
|
|
116
|
-
ghostError = new errors.
|
|
116
|
+
ghostError = new errors.InternalServerError({
|
|
117
117
|
message: tpl(messages.otherError.error, {errorNumber: error.errno}),
|
|
118
118
|
context: tpl(messages.otherError.context),
|
|
119
119
|
help: tpl(messages.otherError.help)
|
|
@@ -177,7 +177,7 @@ class ImageSize {
|
|
|
177
177
|
context: err.url || imagePath
|
|
178
178
|
}));
|
|
179
179
|
}).catch(function (err) {
|
|
180
|
-
if (errors.utils.
|
|
180
|
+
if (errors.utils.isGhostError(err)) {
|
|
181
181
|
return Promise.reject(err);
|
|
182
182
|
}
|
|
183
183
|
|
|
@@ -241,7 +241,7 @@ class ImageSize {
|
|
|
241
241
|
}
|
|
242
242
|
}));
|
|
243
243
|
}).catch((err) => {
|
|
244
|
-
if (errors.utils.
|
|
244
|
+
if (errors.utils.isGhostError(err)) {
|
|
245
245
|
return Promise.reject(err);
|
|
246
246
|
}
|
|
247
247
|
|
|
@@ -69,13 +69,13 @@ events.on('settings.timezone.edited', function (settingModel, options) {
|
|
|
69
69
|
try {
|
|
70
70
|
await models.Post.edit(post.toJSON(), _.merge({id: post.id}, options));
|
|
71
71
|
} catch (err) {
|
|
72
|
-
logging.error(new errors.
|
|
72
|
+
logging.error(new errors.InternalServerError({
|
|
73
73
|
err
|
|
74
74
|
}));
|
|
75
75
|
}
|
|
76
76
|
});
|
|
77
77
|
} catch (err) {
|
|
78
|
-
logging.error(new errors.
|
|
78
|
+
logging.error(new errors.InternalServerError({
|
|
79
79
|
err: err,
|
|
80
80
|
level: 'critical'
|
|
81
81
|
}));
|
|
@@ -9,11 +9,11 @@ const MemberEmailChangeEvent = ghostBookshelf.Model.extend({
|
|
|
9
9
|
}
|
|
10
10
|
}, {
|
|
11
11
|
async edit() {
|
|
12
|
-
throw new errors.IncorrectUsageError('Cannot edit MemberEmailChangeEvent');
|
|
12
|
+
throw new errors.IncorrectUsageError({message: 'Cannot edit MemberEmailChangeEvent'});
|
|
13
13
|
},
|
|
14
14
|
|
|
15
15
|
async destroy() {
|
|
16
|
-
throw new errors.IncorrectUsageError('Cannot destroy MemberEmailChangeEvent');
|
|
16
|
+
throw new errors.IncorrectUsageError({message: 'Cannot destroy MemberEmailChangeEvent'});
|
|
17
17
|
}
|
|
18
18
|
});
|
|
19
19
|
|
|
@@ -9,11 +9,11 @@ const MemberLoginEvent = ghostBookshelf.Model.extend({
|
|
|
9
9
|
}
|
|
10
10
|
}, {
|
|
11
11
|
async edit() {
|
|
12
|
-
throw new errors.IncorrectUsageError('Cannot edit MemberLoginEvent');
|
|
12
|
+
throw new errors.IncorrectUsageError({message: 'Cannot edit MemberLoginEvent'});
|
|
13
13
|
},
|
|
14
14
|
|
|
15
15
|
async destroy() {
|
|
16
|
-
throw new errors.IncorrectUsageError('Cannot destroy MemberLoginEvent');
|
|
16
|
+
throw new errors.IncorrectUsageError({message: 'Cannot destroy MemberLoginEvent'});
|
|
17
17
|
}
|
|
18
18
|
});
|
|
19
19
|
|
|
@@ -11,7 +11,7 @@ const MemberPaidSubscriptionEvent = ghostBookshelf.Model.extend({
|
|
|
11
11
|
customQuery(qb, options) {
|
|
12
12
|
if (options.aggregateMRRDeltas) {
|
|
13
13
|
if (options.limit || options.filter) {
|
|
14
|
-
throw new errors.IncorrectUsageError('aggregateMRRDeltas does not work when passed a filter or limit');
|
|
14
|
+
throw new errors.IncorrectUsageError({message: 'aggregateMRRDeltas does not work when passed a filter or limit'});
|
|
15
15
|
}
|
|
16
16
|
const knex = ghostBookshelf.knex;
|
|
17
17
|
return qb.clear('select')
|
|
@@ -33,11 +33,11 @@ const MemberPaidSubscriptionEvent = ghostBookshelf.Model.extend({
|
|
|
33
33
|
return options;
|
|
34
34
|
},
|
|
35
35
|
async edit() {
|
|
36
|
-
throw new errors.IncorrectUsageError('Cannot edit MemberPaidSubscriptionEvent');
|
|
36
|
+
throw new errors.IncorrectUsageError({message: 'Cannot edit MemberPaidSubscriptionEvent'});
|
|
37
37
|
},
|
|
38
38
|
|
|
39
39
|
async destroy() {
|
|
40
|
-
throw new errors.IncorrectUsageError('Cannot destroy MemberPaidSubscriptionEvent');
|
|
40
|
+
throw new errors.IncorrectUsageError({message: 'Cannot destroy MemberPaidSubscriptionEvent'});
|
|
41
41
|
}
|
|
42
42
|
});
|
|
43
43
|
|
|
@@ -11,7 +11,7 @@ const MemberPaymentEvent = ghostBookshelf.Model.extend({
|
|
|
11
11
|
customQuery(qb, options) {
|
|
12
12
|
if (options.aggregatePaymentVolume) {
|
|
13
13
|
if (options.limit || options.filter) {
|
|
14
|
-
throw new errors.IncorrectUsageError('aggregatePaymentVolume does not work when passed a filter or limit');
|
|
14
|
+
throw new errors.IncorrectUsageError({message: 'aggregatePaymentVolume does not work when passed a filter or limit'});
|
|
15
15
|
}
|
|
16
16
|
const knex = ghostBookshelf.knex;
|
|
17
17
|
return qb.clear('select')
|
|
@@ -33,11 +33,11 @@ const MemberPaymentEvent = ghostBookshelf.Model.extend({
|
|
|
33
33
|
return options;
|
|
34
34
|
},
|
|
35
35
|
async edit() {
|
|
36
|
-
throw new errors.IncorrectUsageError('Cannot edit MemberPaymentEvent');
|
|
36
|
+
throw new errors.IncorrectUsageError({message: 'Cannot edit MemberPaymentEvent'});
|
|
37
37
|
},
|
|
38
38
|
|
|
39
39
|
async destroy() {
|
|
40
|
-
throw new errors.IncorrectUsageError('Cannot destroy MemberPaymentEvent');
|
|
40
|
+
throw new errors.IncorrectUsageError({message: 'Cannot destroy MemberPaymentEvent'});
|
|
41
41
|
}
|
|
42
42
|
});
|
|
43
43
|
|
|
@@ -19,15 +19,15 @@ const MemberProductEvent = ghostBookshelf.Model.extend({
|
|
|
19
19
|
|
|
20
20
|
}, {
|
|
21
21
|
async edit() {
|
|
22
|
-
throw new errors.IncorrectUsageError(
|
|
23
|
-
tpl(messages.cannotPerformAction, 'edit')
|
|
24
|
-
);
|
|
22
|
+
throw new errors.IncorrectUsageError({
|
|
23
|
+
message: tpl(messages.cannotPerformAction, 'edit')
|
|
24
|
+
});
|
|
25
25
|
},
|
|
26
26
|
|
|
27
27
|
async destroy() {
|
|
28
|
-
throw new errors.IncorrectUsageError(
|
|
29
|
-
tpl(messages.cannotPerformAction, 'destroy')
|
|
30
|
-
);
|
|
28
|
+
throw new errors.IncorrectUsageError({
|
|
29
|
+
message: tpl(messages.cannotPerformAction, 'destroy')
|
|
30
|
+
});
|
|
31
31
|
}
|
|
32
32
|
});
|
|
33
33
|
|
|
@@ -11,7 +11,9 @@ const MemberStatusEvent = ghostBookshelf.Model.extend({
|
|
|
11
11
|
customQuery(qb, options) {
|
|
12
12
|
if (options.aggregateStatusCounts) {
|
|
13
13
|
if (options.limit || options.filter) {
|
|
14
|
-
throw new errors.IncorrectUsageError(
|
|
14
|
+
throw new errors.IncorrectUsageError({
|
|
15
|
+
message: 'aggregateStatusCounts does not work when passed a filter or limit'
|
|
16
|
+
});
|
|
15
17
|
}
|
|
16
18
|
const knex = ghostBookshelf.knex;
|
|
17
19
|
return qb.clear('select')
|
|
@@ -46,11 +48,11 @@ const MemberStatusEvent = ghostBookshelf.Model.extend({
|
|
|
46
48
|
return options;
|
|
47
49
|
},
|
|
48
50
|
async edit() {
|
|
49
|
-
throw new errors.IncorrectUsageError('Cannot edit MemberStatusEvent');
|
|
51
|
+
throw new errors.IncorrectUsageError({message: 'Cannot edit MemberStatusEvent'});
|
|
50
52
|
},
|
|
51
53
|
|
|
52
54
|
async destroy() {
|
|
53
|
-
throw new errors.IncorrectUsageError('Cannot destroy MemberStatusEvent');
|
|
55
|
+
throw new errors.IncorrectUsageError({message: 'Cannot destroy MemberStatusEvent'});
|
|
54
56
|
}
|
|
55
57
|
});
|
|
56
58
|
|
|
@@ -11,7 +11,9 @@ const MemberSubscribeEvent = ghostBookshelf.Model.extend({
|
|
|
11
11
|
customQuery(qb, options) {
|
|
12
12
|
if (options.aggregateSubscriptionDeltas) {
|
|
13
13
|
if (options.limit || options.filter) {
|
|
14
|
-
throw new errors.IncorrectUsageError(
|
|
14
|
+
throw new errors.IncorrectUsageError({
|
|
15
|
+
message: 'aggregateSubscriptionDeltas does not work when passed a filter or limit'
|
|
16
|
+
});
|
|
15
17
|
}
|
|
16
18
|
const knex = ghostBookshelf.knex;
|
|
17
19
|
return qb.clear('select')
|
|
@@ -32,11 +34,15 @@ const MemberSubscribeEvent = ghostBookshelf.Model.extend({
|
|
|
32
34
|
return options;
|
|
33
35
|
},
|
|
34
36
|
async edit() {
|
|
35
|
-
throw new errors.IncorrectUsageError(
|
|
37
|
+
throw new errors.IncorrectUsageError({
|
|
38
|
+
message: 'Cannot edit MemberSubscribeEvent'
|
|
39
|
+
});
|
|
36
40
|
},
|
|
37
41
|
|
|
38
42
|
async destroy() {
|
|
39
|
-
throw new errors.IncorrectUsageError(
|
|
43
|
+
throw new errors.IncorrectUsageError({
|
|
44
|
+
message: 'Cannot destroy MemberSubscribeEvent'
|
|
45
|
+
});
|
|
40
46
|
}
|
|
41
47
|
});
|
|
42
48
|
|
|
@@ -324,7 +324,7 @@ module.exports.extendModel = function extendModel(Post, Posts, ghostBookshelf) {
|
|
|
324
324
|
.then(() => response);
|
|
325
325
|
})
|
|
326
326
|
.catch((err) => {
|
|
327
|
-
throw new errors.
|
|
327
|
+
throw new errors.InternalServerError({err: err});
|
|
328
328
|
});
|
|
329
329
|
});
|
|
330
330
|
|
|
@@ -346,7 +346,7 @@ Settings = ghostBookshelf.Model.extend({
|
|
|
346
346
|
);
|
|
347
347
|
|
|
348
348
|
if (validationErrors.length) {
|
|
349
|
-
throw new errors.ValidationError(validationErrors.join('\n'));
|
|
349
|
+
throw new errors.ValidationError({message: validationErrors.join('\n')});
|
|
350
350
|
}
|
|
351
351
|
},
|
|
352
352
|
async labs(model) {
|
|
@@ -148,7 +148,7 @@ function doReset(options, tokenParts, settingsAPI) {
|
|
|
148
148
|
return Promise.reject(err);
|
|
149
149
|
})
|
|
150
150
|
.catch((err) => {
|
|
151
|
-
if (errors.utils.
|
|
151
|
+
if (errors.utils.isGhostError(err)) {
|
|
152
152
|
return Promise.reject(err);
|
|
153
153
|
}
|
|
154
154
|
return Promise.reject(new errors.UnauthorizedError({err: err}));
|
|
@@ -59,7 +59,7 @@ async function setupUser(userData) {
|
|
|
59
59
|
const owner = await models.User.findOne({role: 'Owner', status: 'all'});
|
|
60
60
|
|
|
61
61
|
if (!owner) {
|
|
62
|
-
throw new errors.
|
|
62
|
+
throw new errors.InternalServerError({
|
|
63
63
|
message: tpl(messages.setupUnableToRun)
|
|
64
64
|
});
|
|
65
65
|
}
|
|
@@ -12,7 +12,7 @@ module.exports = {
|
|
|
12
12
|
!hasScheduled &&
|
|
13
13
|
config.get('emailAnalytics') &&
|
|
14
14
|
config.get('backgroundJobs:emailAnalytics') &&
|
|
15
|
-
!process.env.NODE_ENV.
|
|
15
|
+
!process.env.NODE_ENV.startsWith('test')
|
|
16
16
|
) {
|
|
17
17
|
// Don't register email analytics job if we have no emails,
|
|
18
18
|
// processor usage from many sites spinning up threads can be high.
|