ghost 5.19.3 → 5.20.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-manager-5.20.0.tgz +0 -0
- package/components/{tryghost-api-framework-5.19.3.tgz → tryghost-api-framework-5.20.0.tgz} +0 -0
- package/components/tryghost-api-version-compatibility-service-5.20.0.tgz +0 -0
- package/components/tryghost-audience-feedback-5.20.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.20.0.tgz +0 -0
- package/components/tryghost-constants-5.20.0.tgz +0 -0
- package/components/{tryghost-custom-theme-settings-service-5.19.3.tgz → tryghost-custom-theme-settings-service-5.20.0.tgz} +0 -0
- package/components/tryghost-domain-events-5.20.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.20.0.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.20.0.tgz +0 -0
- package/components/tryghost-email-content-generator-5.20.0.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.20.0.tgz +0 -0
- package/components/tryghost-extract-api-key-5.20.0.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.20.0.tgz +0 -0
- package/components/{tryghost-job-manager-5.19.3.tgz → tryghost-job-manager-5.20.0.tgz} +0 -0
- package/components/tryghost-link-redirects-5.20.0.tgz +0 -0
- package/components/tryghost-link-replacer-5.20.0.tgz +0 -0
- package/components/tryghost-link-tracking-5.20.0.tgz +0 -0
- package/components/tryghost-magic-link-5.20.0.tgz +0 -0
- package/components/tryghost-mailgun-client-5.20.0.tgz +0 -0
- package/components/tryghost-member-analytics-service-5.20.0.tgz +0 -0
- package/components/tryghost-member-attribution-5.20.0.tgz +0 -0
- package/components/tryghost-member-events-5.20.0.tgz +0 -0
- package/components/tryghost-members-analytics-ingress-5.20.0.tgz +0 -0
- package/components/tryghost-members-api-5.20.0.tgz +0 -0
- package/components/tryghost-members-csv-5.20.0.tgz +0 -0
- package/components/tryghost-members-events-service-5.20.0.tgz +0 -0
- package/components/tryghost-members-importer-5.20.0.tgz +0 -0
- package/components/tryghost-members-offers-5.20.0.tgz +0 -0
- package/components/tryghost-members-payments-5.20.0.tgz +0 -0
- package/components/tryghost-members-ssr-5.20.0.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.20.0.tgz +0 -0
- package/components/tryghost-minifier-5.20.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.20.0.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.20.0.tgz +0 -0
- package/components/{tryghost-mw-error-handler-5.19.3.tgz → tryghost-mw-error-handler-5.20.0.tgz} +0 -0
- package/components/tryghost-mw-session-from-token-5.20.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.20.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.20.0.tgz +0 -0
- package/components/tryghost-oembed-service-5.20.0.tgz +0 -0
- package/components/tryghost-package-json-5.20.0.tgz +0 -0
- package/components/{tryghost-referrers-5.19.3.tgz → tryghost-referrers-5.20.0.tgz} +0 -0
- package/components/tryghost-security-5.20.0.tgz +0 -0
- package/components/tryghost-session-service-5.20.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.20.0.tgz +0 -0
- package/components/tryghost-staff-service-5.20.0.tgz +0 -0
- package/components/tryghost-stats-service-5.20.0.tgz +0 -0
- package/components/tryghost-tiers-5.20.0.tgz +0 -0
- package/components/tryghost-update-check-service-5.20.0.tgz +0 -0
- package/components/tryghost-verification-trigger-5.20.0.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.20.0.tgz +0 -0
- package/core/built/admin/assets/{chunk.143.c035c61595ed02eee886.js → chunk.143.d245b085ad1efed4ee76.js} +7 -7
- package/core/built/admin/assets/{chunk.178.998dfbcebcec635146b1.js → chunk.178.c45f56ea31775e509497.js} +4 -4
- package/core/built/admin/assets/{chunk.613.f1d519ad47e7f9024263.js → chunk.613.c4d89dc2d28c1b20348f.js} +3 -3
- package/core/built/admin/assets/{chunk.613.f1d519ad47e7f9024263.js.LICENSE.txt → chunk.613.c4d89dc2d28c1b20348f.js.LICENSE.txt} +0 -0
- package/core/built/admin/assets/{ghost-5ce6f5a730c83c91fc258b12c537ea35.js → ghost-07e4bbf5029630b3c8a8a50c4b9f2d9e.js} +2746 -2658
- package/core/built/admin/assets/{ghost-dark-41929e4857de411a23597a9de49a4e4f.css → ghost-dark-363185f15c782b4b8394c5db23984e7f.css} +1 -1
- package/core/built/admin/assets/{ghost-982146a4ada3a5af1981d1919ae01d08.css → ghost-fd0480352bf27e013b2b00a1bf9ffe84.css} +1 -1
- package/core/built/admin/assets/{vendor-5c7d7063620bec13668c4370145cd4b4.js → vendor-518b03b02df9a55706d150627ef1004f.js} +84 -72
- package/core/built/admin/index.html +7 -7
- package/core/server/api/endpoints/links.js +33 -1
- package/core/server/api/endpoints/members.js +1 -4
- package/core/server/api/endpoints/posts-public.js +1 -1
- package/core/server/api/endpoints/posts.js +2 -1
- package/core/server/api/endpoints/utils/serializers/input/posts.js +21 -1
- package/core/server/api/endpoints/utils/serializers/output/index.js +4 -0
- package/core/server/api/endpoints/utils/serializers/output/links.js +5 -0
- package/core/server/api/endpoints/utils/serializers/output/mappers/activity-feed-events.js +47 -15
- package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +15 -2
- package/core/server/api/endpoints/utils/serializers/output/mappers/snippets.js +2 -2
- package/core/server/api/endpoints/utils/serializers/output/members.js +6 -5
- package/core/server/data/importer/importers/data/custom-theme-settings.js +81 -0
- package/core/server/data/importer/importers/data/data-importer.js +2 -0
- package/core/server/data/migrations/utils/permissions.js +35 -24
- package/core/server/data/migrations/versions/5.20/2022-10-18-05-39-drop-nullable-tier-id.js +3 -0
- package/core/server/data/migrations/versions/5.20/2022-10-18-10-13-add-ghost-subscription-id-column-to-mscs.js +10 -0
- package/core/server/data/migrations/versions/5.20/2022-10-19-11-17-add-link-browse-permissions.js +10 -0
- package/core/server/data/migrations/versions/5.20/2022-10-20-02-52-add-link-edit-permissions.js +10 -0
- package/core/server/data/schema/commands.js +107 -48
- package/core/server/data/schema/fixtures/fixtures.json +14 -2
- package/core/server/data/schema/schema.js +2 -1
- package/core/server/models/base/plugins/actions.js +1 -1
- package/core/server/models/email-recipient.js +14 -0
- package/core/server/models/email.js +1 -5
- package/core/server/models/member-click-event.js +24 -0
- package/core/server/models/member-paid-subscription-event.js +15 -0
- package/core/server/models/post.js +32 -1
- package/core/server/models/redirect.js +1 -0
- package/core/server/services/audience-feedback/index.js +2 -0
- package/core/server/services/link-redirection/LinkRedirectRepository.js +16 -5
- package/core/server/services/link-tracking/PostLinkRepository.js +26 -2
- package/core/server/services/link-tracking/index.js +3 -1
- package/core/server/services/members/api.js +2 -1
- package/core/server/services/url/UrlGenerator.js +4 -2
- package/core/server/web/api/endpoints/admin/routes.js +1 -0
- package/core/shared/config/defaults.json +1 -1
- package/core/shared/labs.js +3 -3
- package/package.json +98 -97
- package/yarn.lock +28 -43
- package/components/tryghost-adapter-manager-5.19.3.tgz +0 -0
- package/components/tryghost-api-version-compatibility-service-5.19.3.tgz +0 -0
- package/components/tryghost-audience-feedback-5.19.3.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.19.3.tgz +0 -0
- package/components/tryghost-constants-5.19.3.tgz +0 -0
- package/components/tryghost-domain-events-5.19.3.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.19.3.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.19.3.tgz +0 -0
- package/components/tryghost-email-content-generator-5.19.3.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.19.3.tgz +0 -0
- package/components/tryghost-extract-api-key-5.19.3.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.19.3.tgz +0 -0
- package/components/tryghost-link-redirects-5.19.3.tgz +0 -0
- package/components/tryghost-link-replacer-5.19.3.tgz +0 -0
- package/components/tryghost-link-tracking-5.19.3.tgz +0 -0
- package/components/tryghost-magic-link-5.19.3.tgz +0 -0
- package/components/tryghost-mailgun-client-5.19.3.tgz +0 -0
- package/components/tryghost-member-analytics-service-5.19.3.tgz +0 -0
- package/components/tryghost-member-attribution-5.19.3.tgz +0 -0
- package/components/tryghost-member-events-5.19.3.tgz +0 -0
- package/components/tryghost-members-analytics-ingress-5.19.3.tgz +0 -0
- package/components/tryghost-members-api-5.19.3.tgz +0 -0
- package/components/tryghost-members-csv-5.19.3.tgz +0 -0
- package/components/tryghost-members-events-service-5.19.3.tgz +0 -0
- package/components/tryghost-members-importer-5.19.3.tgz +0 -0
- package/components/tryghost-members-offers-5.19.3.tgz +0 -0
- package/components/tryghost-members-payments-5.19.3.tgz +0 -0
- package/components/tryghost-members-ssr-5.19.3.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.19.3.tgz +0 -0
- package/components/tryghost-minifier-5.19.3.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.19.3.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.19.3.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.19.3.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.19.3.tgz +0 -0
- package/components/tryghost-mw-vhost-5.19.3.tgz +0 -0
- package/components/tryghost-oembed-service-5.19.3.tgz +0 -0
- package/components/tryghost-package-json-5.19.3.tgz +0 -0
- package/components/tryghost-security-5.19.3.tgz +0 -0
- package/components/tryghost-session-service-5.19.3.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.19.3.tgz +0 -0
- package/components/tryghost-staff-service-5.19.3.tgz +0 -0
- package/components/tryghost-stats-service-5.19.3.tgz +0 -0
- package/components/tryghost-update-check-service-5.19.3.tgz +0 -0
- package/components/tryghost-verification-trigger-5.19.3.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.19.3.tgz +0 -0
|
@@ -14,20 +14,26 @@ const messages = {
|
|
|
14
14
|
noSupportForDatabase: 'No support for database client {client}'
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} tableName
|
|
19
|
+
* @param {import('knex').knex.TableBuilder} tableBuilder
|
|
20
|
+
* @param {string} columnName
|
|
21
|
+
* @param {object} [columnSpec]
|
|
22
|
+
*/
|
|
23
|
+
function addTableColumn(tableName, tableBuilder, columnName, columnSpec = schema[tableName][columnName]) {
|
|
18
24
|
let column;
|
|
19
25
|
|
|
20
26
|
// creation distinguishes between text with fieldtype, string with maxlength and all others
|
|
21
27
|
if (columnSpec.type === 'text' && Object.prototype.hasOwnProperty.call(columnSpec, 'fieldtype')) {
|
|
22
|
-
column =
|
|
28
|
+
column = tableBuilder[columnSpec.type](columnName, columnSpec.fieldtype);
|
|
23
29
|
} else if (columnSpec.type === 'string') {
|
|
24
30
|
if (Object.prototype.hasOwnProperty.call(columnSpec, 'maxlength')) {
|
|
25
|
-
column =
|
|
31
|
+
column = tableBuilder[columnSpec.type](columnName, columnSpec.maxlength);
|
|
26
32
|
} else {
|
|
27
|
-
column =
|
|
33
|
+
column = tableBuilder[columnSpec.type](columnName, 191);
|
|
28
34
|
}
|
|
29
35
|
} else {
|
|
30
|
-
column =
|
|
36
|
+
column = tableBuilder[columnSpec.type](columnName);
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
if (Object.prototype.hasOwnProperty.call(columnSpec, 'nullable') && columnSpec.nullable === true) {
|
|
@@ -48,6 +54,10 @@ function addTableColumn(tableName, table, columnName, columnSpec = schema[tableN
|
|
|
48
54
|
// check if table exists?
|
|
49
55
|
column.references(columnSpec.references);
|
|
50
56
|
}
|
|
57
|
+
if (Object.prototype.hasOwnProperty.call(columnSpec, 'constraintName')) {
|
|
58
|
+
column.withKeyName(columnSpec.constraintName);
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
if (Object.prototype.hasOwnProperty.call(columnSpec, 'cascadeDelete') && columnSpec.cascadeDelete === true) {
|
|
52
62
|
column.onDelete('CASCADE');
|
|
53
63
|
} else if (Object.prototype.hasOwnProperty.call(columnSpec, 'setNullDelete') && columnSpec.setNullDelete === true) {
|
|
@@ -61,18 +71,34 @@ function addTableColumn(tableName, table, columnName, columnSpec = schema[tableN
|
|
|
61
71
|
}
|
|
62
72
|
}
|
|
63
73
|
|
|
74
|
+
/**
|
|
75
|
+
* @param {string} tableName
|
|
76
|
+
* @param {string} column
|
|
77
|
+
* @param {import('knex').Knex} [transaction]
|
|
78
|
+
*/
|
|
64
79
|
function setNullable(tableName, column, transaction = db.knex) {
|
|
65
80
|
return transaction.schema.table(tableName, function (table) {
|
|
66
81
|
table.setNullable(column);
|
|
67
82
|
});
|
|
68
83
|
}
|
|
69
84
|
|
|
85
|
+
/**
|
|
86
|
+
* @param {string} tableName
|
|
87
|
+
* @param {string} column
|
|
88
|
+
* @param {import('knex').Knex} [transaction]
|
|
89
|
+
*/
|
|
70
90
|
function dropNullable(tableName, column, transaction = db.knex) {
|
|
71
91
|
return transaction.schema.table(tableName, function (table) {
|
|
72
92
|
table.dropNullable(column);
|
|
73
93
|
});
|
|
74
94
|
}
|
|
75
95
|
|
|
96
|
+
/**
|
|
97
|
+
* @param {string} tableName
|
|
98
|
+
* @param {string} column
|
|
99
|
+
* @param {import('knex').Knex.Transaction} [transaction]
|
|
100
|
+
* @param {object} columnSpec
|
|
101
|
+
*/
|
|
76
102
|
async function addColumn(tableName, column, transaction = db.knex, columnSpec) {
|
|
77
103
|
const addColumnBuilder = transaction.schema.table(tableName, function (table) {
|
|
78
104
|
addTableColumn(tableName, table, column, columnSpec);
|
|
@@ -85,41 +111,51 @@ async function addColumn(tableName, column, transaction = db.knex, columnSpec) {
|
|
|
85
111
|
return;
|
|
86
112
|
}
|
|
87
113
|
|
|
88
|
-
|
|
114
|
+
for (const sqlQuery of addColumnBuilder.toSQL()) {
|
|
115
|
+
let sql = sqlQuery.sql;
|
|
89
116
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
117
|
+
if (DatabaseInfo.isMySQL(transaction)) {
|
|
118
|
+
// Guard against an ending semicolon
|
|
119
|
+
sql = sql.replace(/;\s*$/, '') + ', algorithm=copy';
|
|
120
|
+
}
|
|
94
121
|
|
|
95
|
-
|
|
122
|
+
await transaction.raw(sql);
|
|
123
|
+
}
|
|
96
124
|
}
|
|
97
125
|
|
|
126
|
+
/**
|
|
127
|
+
* @param {string} tableName
|
|
128
|
+
* @param {string} column
|
|
129
|
+
* @param {import('knex').Knex} [transaction]
|
|
130
|
+
* @param {object} [columnSpec]
|
|
131
|
+
*/
|
|
98
132
|
async function dropColumn(tableName, column, transaction = db.knex, columnSpec = {}) {
|
|
99
133
|
if (Object.prototype.hasOwnProperty.call(columnSpec, 'references')) {
|
|
100
134
|
const [toTable, toColumn] = columnSpec.references.split('.');
|
|
101
|
-
await dropForeign({fromTable: tableName, fromColumn: column, toTable, toColumn, transaction});
|
|
135
|
+
await dropForeign({fromTable: tableName, fromColumn: column, toTable, toColumn, constraintName: columnSpec.constraintName, transaction});
|
|
102
136
|
}
|
|
103
137
|
|
|
104
|
-
const
|
|
138
|
+
const dropColumnBuilder = transaction.schema.table(tableName, function (table) {
|
|
105
139
|
table.dropColumn(column);
|
|
106
140
|
});
|
|
107
141
|
|
|
108
142
|
// Use the default flow for SQLite because .toSQL() is tricky with SQLite when
|
|
109
143
|
// it does the table dance
|
|
110
144
|
if (DatabaseInfo.isSQLite(transaction)) {
|
|
111
|
-
await
|
|
145
|
+
await dropColumnBuilder;
|
|
112
146
|
return;
|
|
113
147
|
}
|
|
114
148
|
|
|
115
|
-
|
|
149
|
+
for (const sqlQuery of dropColumnBuilder.toSQL()) {
|
|
150
|
+
let sql = sqlQuery.sql;
|
|
116
151
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
152
|
+
if (DatabaseInfo.isMySQL(transaction)) {
|
|
153
|
+
// Guard against an ending semicolon
|
|
154
|
+
sql = sql.replace(/;\s*$/, '') + ', algorithm=copy';
|
|
155
|
+
}
|
|
121
156
|
|
|
122
|
-
|
|
157
|
+
await transaction.raw(sql);
|
|
158
|
+
}
|
|
123
159
|
}
|
|
124
160
|
|
|
125
161
|
/**
|
|
@@ -127,7 +163,7 @@ async function dropColumn(tableName, column, transaction = db.knex, columnSpec =
|
|
|
127
163
|
*
|
|
128
164
|
* @param {string} tableName - name of the table to add unique constraint to
|
|
129
165
|
* @param {string|string[]} columns - column(s) to form unique constraint with
|
|
130
|
-
* @param {import('knex')} transaction - connection object containing knex reference
|
|
166
|
+
* @param {import('knex').Knex} [transaction] - connection object containing knex reference
|
|
131
167
|
*/
|
|
132
168
|
async function addUnique(tableName, columns, transaction = db.knex) {
|
|
133
169
|
try {
|
|
@@ -154,7 +190,7 @@ async function addUnique(tableName, columns, transaction = db.knex) {
|
|
|
154
190
|
*
|
|
155
191
|
* @param {string} tableName - name of the table to drop unique constraint from
|
|
156
192
|
* @param {string|string[]} columns - column(s) unique constraint was formed
|
|
157
|
-
* @param {import('knex')} transaction - connection object containing knex reference
|
|
193
|
+
* @param {import('knex').Knex} transaction - connection object containing knex reference
|
|
158
194
|
*/
|
|
159
195
|
async function dropUnique(tableName, columns, transaction = db.knex) {
|
|
160
196
|
try {
|
|
@@ -184,7 +220,7 @@ async function dropUnique(tableName, columns, transaction = db.knex) {
|
|
|
184
220
|
* @param {string} configuration.fromColumn - column of the table to add the foreign key to
|
|
185
221
|
* @param {string} configuration.toTable - name of the table to point the foreign key to
|
|
186
222
|
* @param {string} configuration.toColumn - column of the table to point the foreign key to
|
|
187
|
-
* @param {import('knex')} configuration.transaction - connection object containing knex reference
|
|
223
|
+
* @param {import('knex').Knex} [configuration.transaction] - connection object containing knex reference
|
|
188
224
|
*/
|
|
189
225
|
async function hasForeignSQLite({fromTable, fromColumn, toTable, toColumn, transaction = db.knex}) {
|
|
190
226
|
if (!DatabaseInfo.isSQLite(transaction)) {
|
|
@@ -208,11 +244,12 @@ async function hasForeignSQLite({fromTable, fromColumn, toTable, toColumn, trans
|
|
|
208
244
|
* @param {string} configuration.fromColumn - column of the table to add the foreign key to
|
|
209
245
|
* @param {string} configuration.toTable - name of the table to point the foreign key to
|
|
210
246
|
* @param {string} configuration.toColumn - column of the table to point the foreign key to
|
|
247
|
+
* @param {string} [configuration.constraintName] - name of the FK to create
|
|
211
248
|
* @param {Boolean} [configuration.cascadeDelete] - adds the "on delete cascade" option if true
|
|
212
249
|
* @param {Boolean} [configuration.setNullDelete] - adds the "on delete SET NULL" option if true
|
|
213
|
-
* @param {import('knex')} configuration.transaction - connection object containing knex reference
|
|
250
|
+
* @param {import('knex').Knex} [configuration.transaction] - connection object containing knex reference
|
|
214
251
|
*/
|
|
215
|
-
async function addForeign({fromTable, fromColumn, toTable, toColumn, cascadeDelete = false, setNullDelete = false, transaction = db.knex}) {
|
|
252
|
+
async function addForeign({fromTable, fromColumn, toTable, toColumn, constraintName, cascadeDelete = false, setNullDelete = false, transaction = db.knex}) {
|
|
216
253
|
if (DatabaseInfo.isSQLite(transaction)) {
|
|
217
254
|
const foreignKeyExists = await hasForeignSQLite({fromTable, fromColumn, toTable, toColumn, transaction});
|
|
218
255
|
if (foreignKeyExists) {
|
|
@@ -233,12 +270,18 @@ async function addForeign({fromTable, fromColumn, toTable, toColumn, cascadeDele
|
|
|
233
270
|
}
|
|
234
271
|
|
|
235
272
|
await transaction.schema.table(fromTable, function (table) {
|
|
273
|
+
let fkBuilder;
|
|
274
|
+
|
|
236
275
|
if (cascadeDelete) {
|
|
237
|
-
table.foreign(fromColumn).references(`${toTable}.${toColumn}`).onDelete('CASCADE');
|
|
276
|
+
fkBuilder = table.foreign(fromColumn).references(`${toTable}.${toColumn}`).onDelete('CASCADE');
|
|
238
277
|
} else if (setNullDelete) {
|
|
239
|
-
table.foreign(fromColumn).references(`${toTable}.${toColumn}`).onDelete('SET NULL');
|
|
278
|
+
fkBuilder = table.foreign(fromColumn).references(`${toTable}.${toColumn}`).onDelete('SET NULL');
|
|
240
279
|
} else {
|
|
241
|
-
table.foreign(fromColumn).references(`${toTable}.${toColumn}`);
|
|
280
|
+
fkBuilder = table.foreign(fromColumn).references(`${toTable}.${toColumn}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (constraintName) {
|
|
284
|
+
fkBuilder.withKeyName(constraintName);
|
|
242
285
|
}
|
|
243
286
|
});
|
|
244
287
|
|
|
@@ -264,9 +307,10 @@ async function addForeign({fromTable, fromColumn, toTable, toColumn, cascadeDele
|
|
|
264
307
|
* @param {string} configuration.fromColumn - column of the table to add the foreign key to
|
|
265
308
|
* @param {string} configuration.toTable - name of the table to point the foreign key to
|
|
266
309
|
* @param {string} configuration.toColumn - column of the table to point the foreign key to
|
|
267
|
-
* @param {
|
|
310
|
+
* @param {string} [configuration.constraintName] - name of the FK to delete
|
|
311
|
+
* @param {import('knex').Knex} [configuration.transaction] - connection object containing knex reference
|
|
268
312
|
*/
|
|
269
|
-
async function dropForeign({fromTable, fromColumn, toTable, toColumn, transaction = db.knex}) {
|
|
313
|
+
async function dropForeign({fromTable, fromColumn, toTable, toColumn, constraintName, transaction = db.knex}) {
|
|
270
314
|
if (DatabaseInfo.isSQLite(transaction)) {
|
|
271
315
|
const foreignKeyExists = await hasForeignSQLite({fromTable, fromColumn, toTable, toColumn, transaction});
|
|
272
316
|
if (!foreignKeyExists) {
|
|
@@ -287,7 +331,7 @@ async function dropForeign({fromTable, fromColumn, toTable, toColumn, transactio
|
|
|
287
331
|
}
|
|
288
332
|
|
|
289
333
|
await transaction.schema.table(fromTable, function (table) {
|
|
290
|
-
table.dropForeign(fromColumn);
|
|
334
|
+
table.dropForeign(fromColumn, constraintName);
|
|
291
335
|
});
|
|
292
336
|
|
|
293
337
|
if (DatabaseInfo.isSQLite(transaction)) {
|
|
@@ -308,7 +352,7 @@ async function dropForeign({fromTable, fromColumn, toTable, toColumn, transactio
|
|
|
308
352
|
* Checks if primary key index exists in a table over the given columns.
|
|
309
353
|
*
|
|
310
354
|
* @param {string} tableName - name of the table to check primary key constraint on
|
|
311
|
-
* @param {import('knex')} transaction - connection object containing knex reference
|
|
355
|
+
* @param {import('knex').Knex} [transaction] - connection object containing knex reference
|
|
312
356
|
*/
|
|
313
357
|
async function hasPrimaryKeySQLite(tableName, transaction = db.knex) {
|
|
314
358
|
if (!DatabaseInfo.isSQLite(transaction)){
|
|
@@ -328,7 +372,7 @@ async function hasPrimaryKeySQLite(tableName, transaction = db.knex) {
|
|
|
328
372
|
*
|
|
329
373
|
* @param {string} tableName - name of the table to add primaykey constraint to
|
|
330
374
|
* @param {string|string[]} columns - column(s) to form primary key constraint with
|
|
331
|
-
* @param {import('knex')} transaction - connection object containing knex reference
|
|
375
|
+
* @param {import('knex').Knex} [transaction] - connection object containing knex reference
|
|
332
376
|
*/
|
|
333
377
|
async function addPrimaryKey(tableName, columns, transaction = db.knex) {
|
|
334
378
|
if (DatabaseInfo.isSQLite(transaction)) {
|
|
@@ -359,7 +403,7 @@ async function addPrimaryKey(tableName, columns, transaction = db.knex) {
|
|
|
359
403
|
* utils if you want that
|
|
360
404
|
*
|
|
361
405
|
* @param {String} table - name of the table to create
|
|
362
|
-
* @param {import('knex').
|
|
406
|
+
* @param {import('knex').Knex} [transaction] - connection to the DB
|
|
363
407
|
* @param {Object} [tableSpec] - table schema to generate table with
|
|
364
408
|
*/
|
|
365
409
|
function createTable(table, transaction = db.knex, tableSpec = schema[table]) {
|
|
@@ -377,10 +421,17 @@ function createTable(table, transaction = db.knex, tableSpec = schema[table]) {
|
|
|
377
421
|
});
|
|
378
422
|
}
|
|
379
423
|
|
|
424
|
+
/**
|
|
425
|
+
* @param {string} table
|
|
426
|
+
* @param {import('knex').Knex} [transaction] - connection to the DB
|
|
427
|
+
*/
|
|
380
428
|
function deleteTable(table, transaction = db.knex) {
|
|
381
429
|
return transaction.schema.dropTableIfExists(table);
|
|
382
430
|
}
|
|
383
431
|
|
|
432
|
+
/**
|
|
433
|
+
* @param {import('knex').Knex} [transaction] - connection to the DB
|
|
434
|
+
*/
|
|
384
435
|
function getTables(transaction = db.knex) {
|
|
385
436
|
const client = transaction.client.config.client;
|
|
386
437
|
|
|
@@ -391,6 +442,10 @@ function getTables(transaction = db.knex) {
|
|
|
391
442
|
return Promise.reject(tpl(messages.noSupportForDatabase, {client: client}));
|
|
392
443
|
}
|
|
393
444
|
|
|
445
|
+
/**
|
|
446
|
+
* @param {string} table
|
|
447
|
+
* @param {import('knex').Knex} [transaction] - connection to the DB
|
|
448
|
+
*/
|
|
394
449
|
function getIndexes(table, transaction = db.knex) {
|
|
395
450
|
const client = transaction.client.config.client;
|
|
396
451
|
|
|
@@ -401,6 +456,10 @@ function getIndexes(table, transaction = db.knex) {
|
|
|
401
456
|
return Promise.reject(tpl(messages.noSupportForDatabase, {client: client}));
|
|
402
457
|
}
|
|
403
458
|
|
|
459
|
+
/**
|
|
460
|
+
* @param {string} table
|
|
461
|
+
* @param {import('knex').Knex} [transaction] - connection to the DB
|
|
462
|
+
*/
|
|
404
463
|
function getColumns(table, transaction = db.knex) {
|
|
405
464
|
const client = transaction.client.config.client;
|
|
406
465
|
|
|
@@ -441,20 +500,20 @@ function createColumnMigration(...migrations) {
|
|
|
441
500
|
}
|
|
442
501
|
|
|
443
502
|
module.exports = {
|
|
444
|
-
createTable
|
|
445
|
-
deleteTable
|
|
446
|
-
getTables
|
|
447
|
-
getIndexes
|
|
448
|
-
addUnique
|
|
449
|
-
dropUnique
|
|
450
|
-
addPrimaryKey
|
|
451
|
-
addForeign
|
|
452
|
-
dropForeign
|
|
453
|
-
addColumn
|
|
454
|
-
dropColumn
|
|
455
|
-
setNullable
|
|
456
|
-
dropNullable
|
|
457
|
-
getColumns
|
|
503
|
+
createTable,
|
|
504
|
+
deleteTable,
|
|
505
|
+
getTables,
|
|
506
|
+
getIndexes,
|
|
507
|
+
addUnique,
|
|
508
|
+
dropUnique,
|
|
509
|
+
addPrimaryKey,
|
|
510
|
+
addForeign,
|
|
511
|
+
dropForeign,
|
|
512
|
+
addColumn,
|
|
513
|
+
dropColumn,
|
|
514
|
+
setNullable,
|
|
515
|
+
dropNullable,
|
|
516
|
+
getColumns,
|
|
458
517
|
createColumnMigration,
|
|
459
518
|
// NOTE: below are exposed for testing purposes only
|
|
460
519
|
_hasForeignSQLite: hasForeignSQLite,
|
|
@@ -618,6 +618,16 @@
|
|
|
618
618
|
"name": "Report comments",
|
|
619
619
|
"action_type": "report",
|
|
620
620
|
"object_type": "comment"
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
"name": "Browse links",
|
|
624
|
+
"action_type": "browse",
|
|
625
|
+
"object_type": "link"
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
"name": "Edit links",
|
|
629
|
+
"action_type": "edit",
|
|
630
|
+
"object_type": "link"
|
|
621
631
|
}
|
|
622
632
|
]
|
|
623
633
|
},
|
|
@@ -747,7 +757,8 @@
|
|
|
747
757
|
"members_stripe_connect": "auth",
|
|
748
758
|
"newsletter": "all",
|
|
749
759
|
"explore": "read",
|
|
750
|
-
"comment": "all"
|
|
760
|
+
"comment": "all",
|
|
761
|
+
"link": "all"
|
|
751
762
|
},
|
|
752
763
|
"DB Backup Integration": {
|
|
753
764
|
"db": "all"
|
|
@@ -781,7 +792,8 @@
|
|
|
781
792
|
"offer": ["browse", "read", "add", "edit"],
|
|
782
793
|
"newsletter": ["browse", "read", "add", "edit"],
|
|
783
794
|
"explore": "read",
|
|
784
|
-
"comment": "all"
|
|
795
|
+
"comment": "all",
|
|
796
|
+
"link": "all"
|
|
785
797
|
},
|
|
786
798
|
"Editor": {
|
|
787
799
|
"notification": "all",
|
|
@@ -630,7 +630,7 @@ module.exports = {
|
|
|
630
630
|
}
|
|
631
631
|
},
|
|
632
632
|
member_id: {type: 'string', maxlength: 24, nullable: false, unique: false, references: 'members.id', cascadeDelete: true},
|
|
633
|
-
tier_id: {type: 'string', maxlength: 24, nullable:
|
|
633
|
+
tier_id: {type: 'string', maxlength: 24, nullable: false, unique: false, references: 'products.id'},
|
|
634
634
|
|
|
635
635
|
// These are null if type !== 'paid'
|
|
636
636
|
cadence: {
|
|
@@ -657,6 +657,7 @@ module.exports = {
|
|
|
657
657
|
members_stripe_customers_subscriptions: {
|
|
658
658
|
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
659
659
|
customer_id: {type: 'string', maxlength: 255, nullable: false, unique: false, references: 'members_stripe_customers.customer_id', cascadeDelete: true},
|
|
660
|
+
ghost_subscription_id: {type: 'string', maxlength: 24, nullable: true, references: 'subscriptions.id', constraintName: 'mscs_ghost_subscription_id_foreign', cascadeDelete: true},
|
|
660
661
|
subscription_id: {type: 'string', maxlength: 255, nullable: false, unique: true},
|
|
661
662
|
stripe_price_id: {type: 'string', maxlength: 255, nullable: false, unique: false, index: true, defaultTo: ''},
|
|
662
663
|
status: {type: 'string', maxlength: 50, nullable: false},
|
|
@@ -72,7 +72,7 @@ module.exports = function (Bookshelf) {
|
|
|
72
72
|
*
|
|
73
73
|
* We protect adding too many and uncontrolled events.
|
|
74
74
|
*
|
|
75
|
-
* We could
|
|
75
|
+
* We could embed adding actions more nicely in the future e.g. plugin.
|
|
76
76
|
*/
|
|
77
77
|
addAction: (model, event, options) => {
|
|
78
78
|
if (!model.wasChanged()) {
|
|
@@ -3,6 +3,20 @@ const ghostBookshelf = require('./base');
|
|
|
3
3
|
const EmailRecipient = ghostBookshelf.Model.extend({
|
|
4
4
|
tableName: 'email_recipients',
|
|
5
5
|
hasTimestamps: false,
|
|
6
|
+
|
|
7
|
+
filterRelations: function filterRelations() {
|
|
8
|
+
return {
|
|
9
|
+
email: {
|
|
10
|
+
// Mongo-knex doesn't support belongsTo relations
|
|
11
|
+
tableName: 'emails',
|
|
12
|
+
tableNameAs: 'email',
|
|
13
|
+
type: 'manyToMany',
|
|
14
|
+
joinTable: 'email_recipients',
|
|
15
|
+
joinFrom: 'id',
|
|
16
|
+
joinTo: 'email_id'
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
},
|
|
6
20
|
|
|
7
21
|
email() {
|
|
8
22
|
return this.belongsTo('Email', 'email_id');
|
|
@@ -10,7 +10,31 @@ const MemberClickEvent = ghostBookshelf.Model.extend({
|
|
|
10
10
|
|
|
11
11
|
member() {
|
|
12
12
|
return this.belongsTo('Member', 'member_id', 'id');
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
filterExpansions: function filterExpansions() {
|
|
16
|
+
const expansions = [{
|
|
17
|
+
key: 'post_id',
|
|
18
|
+
replacement: 'link.post_id'
|
|
19
|
+
}];
|
|
20
|
+
|
|
21
|
+
return expansions;
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
filterRelations() {
|
|
25
|
+
return {
|
|
26
|
+
link: {
|
|
27
|
+
// Mongo-knex doesn't support belongsTo relations
|
|
28
|
+
tableName: 'redirects',
|
|
29
|
+
tableNameAs: 'link',
|
|
30
|
+
type: 'manyToMany',
|
|
31
|
+
joinTable: 'members_click_events',
|
|
32
|
+
joinFrom: 'id',
|
|
33
|
+
joinTo: 'redirect_id'
|
|
34
|
+
}
|
|
35
|
+
};
|
|
13
36
|
}
|
|
37
|
+
|
|
14
38
|
}, {
|
|
15
39
|
async edit() {
|
|
16
40
|
throw new errors.IncorrectUsageError({message: 'Cannot edit MemberClickEvent'});
|
|
@@ -25,6 +25,21 @@ const MemberPaidSubscriptionEvent = ghostBookshelf.Model.extend({
|
|
|
25
25
|
.groupByRaw('currency, DATE(created_at)')
|
|
26
26
|
.orderByRaw('DATE(created_at)');
|
|
27
27
|
}
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
filterRelations() {
|
|
31
|
+
return {
|
|
32
|
+
subscriptionCreatedEvent: {
|
|
33
|
+
// Mongo-knex doesn't support belongsTo relations
|
|
34
|
+
tableName: 'members_subscription_created_events',
|
|
35
|
+
tableNameAs: 'subscriptionCreatedEvent',
|
|
36
|
+
type: 'manyToMany',
|
|
37
|
+
joinTable: 'members_paid_subscription_events',
|
|
38
|
+
joinFrom: 'id',
|
|
39
|
+
joinToForeign: 'subscription_id',
|
|
40
|
+
joinTo: 'subscription_id'
|
|
41
|
+
}
|
|
42
|
+
};
|
|
28
43
|
}
|
|
29
44
|
}, {
|
|
30
45
|
permittedOptions(methodName) {
|
|
@@ -236,6 +236,17 @@ Post = ghostBookshelf.Model.extend({
|
|
|
236
236
|
},
|
|
237
237
|
|
|
238
238
|
orderRawQuery: function orderRawQuery(field, direction, withRelated) {
|
|
239
|
+
if (field === 'sentiment') {
|
|
240
|
+
if (withRelated.includes('count.sentiment')) {
|
|
241
|
+
// Internally sentiment can be included via the count.sentiment relation. We can do a quick optimisation of the query in that case.
|
|
242
|
+
return {
|
|
243
|
+
orderByRaw: `count__sentiment ${direction}`
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
orderByRaw: `(select AVG(score) from \`members_feedback\` where posts.id = members_feedback.post_id) ${direction}`
|
|
248
|
+
};
|
|
249
|
+
}
|
|
239
250
|
if (field === 'email.open_rate' && withRelated && withRelated.indexOf('email') > -1) {
|
|
240
251
|
return {
|
|
241
252
|
// *1.0 is needed on one of the columns to prevent sqlite from
|
|
@@ -1346,6 +1357,26 @@ Post = ghostBookshelf.Model.extend({
|
|
|
1346
1357
|
.as('count__paid_conversions');
|
|
1347
1358
|
});
|
|
1348
1359
|
},
|
|
1360
|
+
/**
|
|
1361
|
+
* Combination of sigups and paid conversions, but unique per member
|
|
1362
|
+
*/
|
|
1363
|
+
conversions(modelOrCollection) {
|
|
1364
|
+
modelOrCollection.query('columns', 'posts.*', (qb) => {
|
|
1365
|
+
qb.count('*')
|
|
1366
|
+
.from('k')
|
|
1367
|
+
.with('k', (q) => {
|
|
1368
|
+
q.select('member_id')
|
|
1369
|
+
.from('members_subscription_created_events')
|
|
1370
|
+
.whereRaw('posts.id = members_subscription_created_events.attribution_id')
|
|
1371
|
+
.union(function () {
|
|
1372
|
+
this.select('member_id')
|
|
1373
|
+
.from('members_created_events')
|
|
1374
|
+
.whereRaw('posts.id = members_created_events.attribution_id');
|
|
1375
|
+
});
|
|
1376
|
+
})
|
|
1377
|
+
.as('count__conversions');
|
|
1378
|
+
});
|
|
1379
|
+
},
|
|
1349
1380
|
clicks(modelOrCollection) {
|
|
1350
1381
|
modelOrCollection.query('columns', 'posts.*', (qb) => {
|
|
1351
1382
|
qb.countDistinct('members_click_events.member_id')
|
|
@@ -1357,7 +1388,7 @@ Post = ghostBookshelf.Model.extend({
|
|
|
1357
1388
|
},
|
|
1358
1389
|
sentiment(modelOrCollection) {
|
|
1359
1390
|
modelOrCollection.query('columns', 'posts.*', (qb) => {
|
|
1360
|
-
qb.select(qb.client.raw('ROUND(AVG(score) * 100)'))
|
|
1391
|
+
qb.select(qb.client.raw('COALESCE(ROUND(AVG(score) * 100), 0)'))
|
|
1361
1392
|
.from('members_feedback')
|
|
1362
1393
|
.whereRaw('posts.id = members_feedback.post_id')
|
|
1363
1394
|
.as('count__sentiment');
|
|
@@ -53,6 +53,7 @@ const Redirect = ghostBookshelf.Model.extend({
|
|
|
53
53
|
qb.countDistinct('members_click_events.member_id')
|
|
54
54
|
.from('members_click_events')
|
|
55
55
|
.whereRaw('redirects.id = members_click_events.redirect_id')
|
|
56
|
+
.whereRaw('redirects.updated_at <= members_click_events.created_at')
|
|
56
57
|
.as('count__clicks');
|
|
57
58
|
});
|
|
58
59
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const urlUtils = require('../../../shared/url-utils');
|
|
2
|
+
const urlService = require('../../services/url');
|
|
2
3
|
const FeedbackRepository = require('./FeedbackRepository');
|
|
3
4
|
|
|
4
5
|
class AudienceFeedbackServiceWrapper {
|
|
@@ -22,6 +23,7 @@ class AudienceFeedbackServiceWrapper {
|
|
|
22
23
|
|
|
23
24
|
// Expose the service
|
|
24
25
|
this.service = new AudienceFeedbackService({
|
|
26
|
+
urlService,
|
|
25
27
|
config: {
|
|
26
28
|
baseURL: new URL(urlUtils.urlFor('home', true))
|
|
27
29
|
}
|
|
@@ -18,7 +18,7 @@ module.exports = class LinkRedirectRepository {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* @param {InstanceType<LinkRedirect>} linkRedirect
|
|
21
|
+
* @param {InstanceType<LinkRedirect>} linkRedirect
|
|
22
22
|
* @returns {Promise<void>}
|
|
23
23
|
*/
|
|
24
24
|
async save(linkRedirect) {
|
|
@@ -36,10 +36,14 @@ module.exports = class LinkRedirectRepository {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
fromModel(model) {
|
|
39
|
+
// Store if link has been edited
|
|
40
|
+
const edited = model.get('created_at')?.getTime() !== model.get('updated_at')?.getTime();
|
|
41
|
+
|
|
39
42
|
return new LinkRedirect({
|
|
40
43
|
id: model.id,
|
|
41
44
|
from: new URL(this.#trimLeadingSlash(model.get('from')), this.#urlUtils.urlFor('home', true)),
|
|
42
|
-
to: new URL(model.get('to'))
|
|
45
|
+
to: new URL(model.get('to')),
|
|
46
|
+
edited
|
|
43
47
|
});
|
|
44
48
|
}
|
|
45
49
|
|
|
@@ -55,10 +59,17 @@ module.exports = class LinkRedirectRepository {
|
|
|
55
59
|
return result;
|
|
56
60
|
}
|
|
57
61
|
|
|
62
|
+
async getFilteredIds(options) {
|
|
63
|
+
const linkRows = await this.#LinkRedirect.getFilteredCollectionQuery(options)
|
|
64
|
+
.select('redirects.id')
|
|
65
|
+
.distinct();
|
|
66
|
+
return linkRows.map(row => row.id);
|
|
67
|
+
}
|
|
68
|
+
|
|
58
69
|
/**
|
|
59
|
-
*
|
|
60
|
-
* @param {URL} url
|
|
61
|
-
* @returns {Promise<InstanceType<LinkRedirect>|undefined>} linkRedirect
|
|
70
|
+
*
|
|
71
|
+
* @param {URL} url
|
|
72
|
+
* @returns {Promise<InstanceType<LinkRedirect>|undefined>} linkRedirect
|
|
62
73
|
*/
|
|
63
74
|
async getByURL(url) {
|
|
64
75
|
// Strip subdirectory from path
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const {FullPostLink} = require('@tryghost/link-tracking');
|
|
2
|
+
const _ = require('lodash');
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* @typedef {import('bson-objectid').default} ObjectID
|
|
@@ -22,8 +23,8 @@ module.exports = class PostLinkRepository {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
|
-
*
|
|
26
|
-
* @param {*} options
|
|
26
|
+
*
|
|
27
|
+
* @param {*} options
|
|
27
28
|
* @returns {Promise<InstanceType<FullPostLink>[]>}
|
|
28
29
|
*/
|
|
29
30
|
async getAll(options) {
|
|
@@ -48,6 +49,29 @@ module.exports = class PostLinkRepository {
|
|
|
48
49
|
return result;
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
async updateLinks(linkIds, updateData, options) {
|
|
53
|
+
const bulkUpdateOptions = _.pick(options, ['transacting']);
|
|
54
|
+
|
|
55
|
+
const bulkActionResult = await this.#LinkRedirect.bulkEdit(linkIds, 'redirects', {
|
|
56
|
+
...bulkUpdateOptions,
|
|
57
|
+
data: updateData
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
bulk: {
|
|
62
|
+
action: 'updateLink',
|
|
63
|
+
meta: {
|
|
64
|
+
stats: {
|
|
65
|
+
successful: bulkActionResult.successful,
|
|
66
|
+
unsuccessful: bulkActionResult.unsuccessful
|
|
67
|
+
},
|
|
68
|
+
errors: bulkActionResult.errors,
|
|
69
|
+
unsuccessfulData: bulkActionResult.unsuccessfulData
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
51
75
|
/**
|
|
52
76
|
* @param {PostLink} postLink
|
|
53
77
|
* @returns {Promise<void>}
|