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.
Files changed (144) hide show
  1. package/components/tryghost-adapter-manager-5.20.0.tgz +0 -0
  2. package/components/{tryghost-api-framework-5.19.3.tgz → tryghost-api-framework-5.20.0.tgz} +0 -0
  3. package/components/tryghost-api-version-compatibility-service-5.20.0.tgz +0 -0
  4. package/components/tryghost-audience-feedback-5.20.0.tgz +0 -0
  5. package/components/tryghost-bootstrap-socket-5.20.0.tgz +0 -0
  6. package/components/tryghost-constants-5.20.0.tgz +0 -0
  7. package/components/{tryghost-custom-theme-settings-service-5.19.3.tgz → tryghost-custom-theme-settings-service-5.20.0.tgz} +0 -0
  8. package/components/tryghost-domain-events-5.20.0.tgz +0 -0
  9. package/components/tryghost-email-analytics-provider-mailgun-5.20.0.tgz +0 -0
  10. package/components/tryghost-email-analytics-service-5.20.0.tgz +0 -0
  11. package/components/tryghost-email-content-generator-5.20.0.tgz +0 -0
  12. package/components/tryghost-express-dynamic-redirects-5.20.0.tgz +0 -0
  13. package/components/tryghost-extract-api-key-5.20.0.tgz +0 -0
  14. package/components/tryghost-html-to-plaintext-5.20.0.tgz +0 -0
  15. package/components/{tryghost-job-manager-5.19.3.tgz → tryghost-job-manager-5.20.0.tgz} +0 -0
  16. package/components/tryghost-link-redirects-5.20.0.tgz +0 -0
  17. package/components/tryghost-link-replacer-5.20.0.tgz +0 -0
  18. package/components/tryghost-link-tracking-5.20.0.tgz +0 -0
  19. package/components/tryghost-magic-link-5.20.0.tgz +0 -0
  20. package/components/tryghost-mailgun-client-5.20.0.tgz +0 -0
  21. package/components/tryghost-member-analytics-service-5.20.0.tgz +0 -0
  22. package/components/tryghost-member-attribution-5.20.0.tgz +0 -0
  23. package/components/tryghost-member-events-5.20.0.tgz +0 -0
  24. package/components/tryghost-members-analytics-ingress-5.20.0.tgz +0 -0
  25. package/components/tryghost-members-api-5.20.0.tgz +0 -0
  26. package/components/tryghost-members-csv-5.20.0.tgz +0 -0
  27. package/components/tryghost-members-events-service-5.20.0.tgz +0 -0
  28. package/components/tryghost-members-importer-5.20.0.tgz +0 -0
  29. package/components/tryghost-members-offers-5.20.0.tgz +0 -0
  30. package/components/tryghost-members-payments-5.20.0.tgz +0 -0
  31. package/components/tryghost-members-ssr-5.20.0.tgz +0 -0
  32. package/components/tryghost-members-stripe-service-5.20.0.tgz +0 -0
  33. package/components/tryghost-minifier-5.20.0.tgz +0 -0
  34. package/components/tryghost-mw-api-version-mismatch-5.20.0.tgz +0 -0
  35. package/components/tryghost-mw-cache-control-5.20.0.tgz +0 -0
  36. package/components/{tryghost-mw-error-handler-5.19.3.tgz → tryghost-mw-error-handler-5.20.0.tgz} +0 -0
  37. package/components/tryghost-mw-session-from-token-5.20.0.tgz +0 -0
  38. package/components/tryghost-mw-update-user-last-seen-5.20.0.tgz +0 -0
  39. package/components/tryghost-mw-vhost-5.20.0.tgz +0 -0
  40. package/components/tryghost-oembed-service-5.20.0.tgz +0 -0
  41. package/components/tryghost-package-json-5.20.0.tgz +0 -0
  42. package/components/{tryghost-referrers-5.19.3.tgz → tryghost-referrers-5.20.0.tgz} +0 -0
  43. package/components/tryghost-security-5.20.0.tgz +0 -0
  44. package/components/tryghost-session-service-5.20.0.tgz +0 -0
  45. package/components/tryghost-settings-path-manager-5.20.0.tgz +0 -0
  46. package/components/tryghost-staff-service-5.20.0.tgz +0 -0
  47. package/components/tryghost-stats-service-5.20.0.tgz +0 -0
  48. package/components/tryghost-tiers-5.20.0.tgz +0 -0
  49. package/components/tryghost-update-check-service-5.20.0.tgz +0 -0
  50. package/components/tryghost-verification-trigger-5.20.0.tgz +0 -0
  51. package/components/tryghost-version-notifications-data-service-5.20.0.tgz +0 -0
  52. package/core/built/admin/assets/{chunk.143.c035c61595ed02eee886.js → chunk.143.d245b085ad1efed4ee76.js} +7 -7
  53. package/core/built/admin/assets/{chunk.178.998dfbcebcec635146b1.js → chunk.178.c45f56ea31775e509497.js} +4 -4
  54. package/core/built/admin/assets/{chunk.613.f1d519ad47e7f9024263.js → chunk.613.c4d89dc2d28c1b20348f.js} +3 -3
  55. package/core/built/admin/assets/{chunk.613.f1d519ad47e7f9024263.js.LICENSE.txt → chunk.613.c4d89dc2d28c1b20348f.js.LICENSE.txt} +0 -0
  56. package/core/built/admin/assets/{ghost-5ce6f5a730c83c91fc258b12c537ea35.js → ghost-07e4bbf5029630b3c8a8a50c4b9f2d9e.js} +2746 -2658
  57. package/core/built/admin/assets/{ghost-dark-41929e4857de411a23597a9de49a4e4f.css → ghost-dark-363185f15c782b4b8394c5db23984e7f.css} +1 -1
  58. package/core/built/admin/assets/{ghost-982146a4ada3a5af1981d1919ae01d08.css → ghost-fd0480352bf27e013b2b00a1bf9ffe84.css} +1 -1
  59. package/core/built/admin/assets/{vendor-5c7d7063620bec13668c4370145cd4b4.js → vendor-518b03b02df9a55706d150627ef1004f.js} +84 -72
  60. package/core/built/admin/index.html +7 -7
  61. package/core/server/api/endpoints/links.js +33 -1
  62. package/core/server/api/endpoints/members.js +1 -4
  63. package/core/server/api/endpoints/posts-public.js +1 -1
  64. package/core/server/api/endpoints/posts.js +2 -1
  65. package/core/server/api/endpoints/utils/serializers/input/posts.js +21 -1
  66. package/core/server/api/endpoints/utils/serializers/output/index.js +4 -0
  67. package/core/server/api/endpoints/utils/serializers/output/links.js +5 -0
  68. package/core/server/api/endpoints/utils/serializers/output/mappers/activity-feed-events.js +47 -15
  69. package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +15 -2
  70. package/core/server/api/endpoints/utils/serializers/output/mappers/snippets.js +2 -2
  71. package/core/server/api/endpoints/utils/serializers/output/members.js +6 -5
  72. package/core/server/data/importer/importers/data/custom-theme-settings.js +81 -0
  73. package/core/server/data/importer/importers/data/data-importer.js +2 -0
  74. package/core/server/data/migrations/utils/permissions.js +35 -24
  75. package/core/server/data/migrations/versions/5.20/2022-10-18-05-39-drop-nullable-tier-id.js +3 -0
  76. package/core/server/data/migrations/versions/5.20/2022-10-18-10-13-add-ghost-subscription-id-column-to-mscs.js +10 -0
  77. package/core/server/data/migrations/versions/5.20/2022-10-19-11-17-add-link-browse-permissions.js +10 -0
  78. package/core/server/data/migrations/versions/5.20/2022-10-20-02-52-add-link-edit-permissions.js +10 -0
  79. package/core/server/data/schema/commands.js +107 -48
  80. package/core/server/data/schema/fixtures/fixtures.json +14 -2
  81. package/core/server/data/schema/schema.js +2 -1
  82. package/core/server/models/base/plugins/actions.js +1 -1
  83. package/core/server/models/email-recipient.js +14 -0
  84. package/core/server/models/email.js +1 -5
  85. package/core/server/models/member-click-event.js +24 -0
  86. package/core/server/models/member-paid-subscription-event.js +15 -0
  87. package/core/server/models/post.js +32 -1
  88. package/core/server/models/redirect.js +1 -0
  89. package/core/server/services/audience-feedback/index.js +2 -0
  90. package/core/server/services/link-redirection/LinkRedirectRepository.js +16 -5
  91. package/core/server/services/link-tracking/PostLinkRepository.js +26 -2
  92. package/core/server/services/link-tracking/index.js +3 -1
  93. package/core/server/services/members/api.js +2 -1
  94. package/core/server/services/url/UrlGenerator.js +4 -2
  95. package/core/server/web/api/endpoints/admin/routes.js +1 -0
  96. package/core/shared/config/defaults.json +1 -1
  97. package/core/shared/labs.js +3 -3
  98. package/package.json +98 -97
  99. package/yarn.lock +28 -43
  100. package/components/tryghost-adapter-manager-5.19.3.tgz +0 -0
  101. package/components/tryghost-api-version-compatibility-service-5.19.3.tgz +0 -0
  102. package/components/tryghost-audience-feedback-5.19.3.tgz +0 -0
  103. package/components/tryghost-bootstrap-socket-5.19.3.tgz +0 -0
  104. package/components/tryghost-constants-5.19.3.tgz +0 -0
  105. package/components/tryghost-domain-events-5.19.3.tgz +0 -0
  106. package/components/tryghost-email-analytics-provider-mailgun-5.19.3.tgz +0 -0
  107. package/components/tryghost-email-analytics-service-5.19.3.tgz +0 -0
  108. package/components/tryghost-email-content-generator-5.19.3.tgz +0 -0
  109. package/components/tryghost-express-dynamic-redirects-5.19.3.tgz +0 -0
  110. package/components/tryghost-extract-api-key-5.19.3.tgz +0 -0
  111. package/components/tryghost-html-to-plaintext-5.19.3.tgz +0 -0
  112. package/components/tryghost-link-redirects-5.19.3.tgz +0 -0
  113. package/components/tryghost-link-replacer-5.19.3.tgz +0 -0
  114. package/components/tryghost-link-tracking-5.19.3.tgz +0 -0
  115. package/components/tryghost-magic-link-5.19.3.tgz +0 -0
  116. package/components/tryghost-mailgun-client-5.19.3.tgz +0 -0
  117. package/components/tryghost-member-analytics-service-5.19.3.tgz +0 -0
  118. package/components/tryghost-member-attribution-5.19.3.tgz +0 -0
  119. package/components/tryghost-member-events-5.19.3.tgz +0 -0
  120. package/components/tryghost-members-analytics-ingress-5.19.3.tgz +0 -0
  121. package/components/tryghost-members-api-5.19.3.tgz +0 -0
  122. package/components/tryghost-members-csv-5.19.3.tgz +0 -0
  123. package/components/tryghost-members-events-service-5.19.3.tgz +0 -0
  124. package/components/tryghost-members-importer-5.19.3.tgz +0 -0
  125. package/components/tryghost-members-offers-5.19.3.tgz +0 -0
  126. package/components/tryghost-members-payments-5.19.3.tgz +0 -0
  127. package/components/tryghost-members-ssr-5.19.3.tgz +0 -0
  128. package/components/tryghost-members-stripe-service-5.19.3.tgz +0 -0
  129. package/components/tryghost-minifier-5.19.3.tgz +0 -0
  130. package/components/tryghost-mw-api-version-mismatch-5.19.3.tgz +0 -0
  131. package/components/tryghost-mw-cache-control-5.19.3.tgz +0 -0
  132. package/components/tryghost-mw-session-from-token-5.19.3.tgz +0 -0
  133. package/components/tryghost-mw-update-user-last-seen-5.19.3.tgz +0 -0
  134. package/components/tryghost-mw-vhost-5.19.3.tgz +0 -0
  135. package/components/tryghost-oembed-service-5.19.3.tgz +0 -0
  136. package/components/tryghost-package-json-5.19.3.tgz +0 -0
  137. package/components/tryghost-security-5.19.3.tgz +0 -0
  138. package/components/tryghost-session-service-5.19.3.tgz +0 -0
  139. package/components/tryghost-settings-path-manager-5.19.3.tgz +0 -0
  140. package/components/tryghost-staff-service-5.19.3.tgz +0 -0
  141. package/components/tryghost-stats-service-5.19.3.tgz +0 -0
  142. package/components/tryghost-update-check-service-5.19.3.tgz +0 -0
  143. package/components/tryghost-verification-trigger-5.19.3.tgz +0 -0
  144. 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
- function addTableColumn(tableName, table, columnName, columnSpec = schema[tableName][columnName]) {
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 = table[columnSpec.type](columnName, columnSpec.fieldtype);
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 = table[columnSpec.type](columnName, columnSpec.maxlength);
31
+ column = tableBuilder[columnSpec.type](columnName, columnSpec.maxlength);
26
32
  } else {
27
- column = table[columnSpec.type](columnName, 191);
33
+ column = tableBuilder[columnSpec.type](columnName, 191);
28
34
  }
29
35
  } else {
30
- column = table[columnSpec.type](columnName);
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
- let sql = addColumnBuilder.toSQL()[0].sql;
114
+ for (const sqlQuery of addColumnBuilder.toSQL()) {
115
+ let sql = sqlQuery.sql;
89
116
 
90
- if (DatabaseInfo.isMySQL(transaction)) {
91
- // Guard against an ending semicolon
92
- sql = sql.replace(/;\s*$/, '') + ', algorithm=copy';
93
- }
117
+ if (DatabaseInfo.isMySQL(transaction)) {
118
+ // Guard against an ending semicolon
119
+ sql = sql.replace(/;\s*$/, '') + ', algorithm=copy';
120
+ }
94
121
 
95
- await transaction.raw(sql);
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 dropTableBuilder = transaction.schema.table(tableName, function (table) {
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 dropTableBuilder;
145
+ await dropColumnBuilder;
112
146
  return;
113
147
  }
114
148
 
115
- let sql = dropTableBuilder.toSQL()[0].sql;
149
+ for (const sqlQuery of dropColumnBuilder.toSQL()) {
150
+ let sql = sqlQuery.sql;
116
151
 
117
- if (DatabaseInfo.isMySQL(transaction)) {
118
- // Guard against an ending semicolon
119
- sql = sql.replace(/;\s*$/, '') + ', algorithm=copy';
120
- }
152
+ if (DatabaseInfo.isMySQL(transaction)) {
153
+ // Guard against an ending semicolon
154
+ sql = sql.replace(/;\s*$/, '') + ', algorithm=copy';
155
+ }
121
156
 
122
- await transaction.raw(sql);
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 {import('knex')} configuration.transaction - connection object containing knex reference
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').Transaction} transaction - connection to the DB
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: createTable,
445
- deleteTable: deleteTable,
446
- getTables: getTables,
447
- getIndexes: getIndexes,
448
- addUnique: addUnique,
449
- dropUnique: dropUnique,
450
- addPrimaryKey: addPrimaryKey,
451
- addForeign: addForeign,
452
- dropForeign: dropForeign,
453
- addColumn: addColumn,
454
- dropColumn: dropColumn,
455
- setNullable: setNullable,
456
- dropNullable: dropNullable,
457
- getColumns: 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: true, unique: false, references: 'products.id'},
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 embedd adding actions more nicely in the future e.g. plugin.
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');
@@ -81,11 +81,7 @@ const Email = ghostBookshelf.Model.extend({
81
81
 
82
82
  model.emitChange('deleted', options);
83
83
  }
84
- }, {
85
- post() {
86
- return this.belongsTo('Post');
87
- }
88
- });
84
+ }, {});
89
85
 
90
86
  const Emails = ghostBookshelf.Collection.extend({
91
87
  model: Email
@@ -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>}