ghost 4.37.0 → 4.39.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 (125) hide show
  1. package/.c8rc.json +1 -1
  2. package/README.md +26 -18
  3. package/content/themes/casper/LICENSE +1 -1
  4. package/content/themes/casper/README.md +1 -1
  5. package/content/themes/casper/assets/built/global.css +1 -1
  6. package/content/themes/casper/assets/built/global.css.map +1 -1
  7. package/content/themes/casper/assets/built/screen.css +1 -1
  8. package/content/themes/casper/assets/built/screen.css.map +1 -1
  9. package/content/themes/casper/assets/css/global.css +14 -6
  10. package/content/themes/casper/assets/css/screen.css +9 -1
  11. package/content/themes/casper/package.json +2 -2
  12. package/content/themes/casper/partials/post-card.hbs +1 -1
  13. package/content/themes/casper/post.hbs +18 -19
  14. package/content/themes/casper/yarn.lock +186 -217
  15. package/core/built/assets/{chunk.3.4906cf0b01d6d8e33374.js → chunk.3.6e2ed2d00856e12bd81a.js} +19 -19
  16. package/core/built/assets/ghost-dark-498ff8339a89bb68c3f78f59bee4146e.css +1 -0
  17. package/core/built/assets/ghost.min-77b93478f83b0def6ddc5a4f23ce963e.css +1 -0
  18. package/core/built/assets/{ghost.min-c1938f6ee696bf08bd6bf93cac341ea2.js → ghost.min-e6559d901897066aa6a6d4145e3728ed.js} +466 -413
  19. package/core/built/assets/icons/{event-changed-subscription.svg → event-subscriptions.svg} +0 -1
  20. package/core/built/assets/icons/eye.svg +4 -1
  21. package/core/built/assets/icons/member-add.svg +3 -0
  22. package/core/built/assets/icons/member.svg +3 -0
  23. package/core/built/assets/icons/pin.svg +4 -1
  24. package/core/built/assets/{vendor.min-6dc30be68238b5c55df0cdc1f2dc8b8d.js → vendor.min-c39476bced9adb98ee2b292d01c7a8f4.js} +2303 -1372
  25. package/core/frontend/apps/private-blogging/lib/middleware.js +1 -1
  26. package/core/frontend/apps/private-blogging/lib/views/private.hbs +9 -10
  27. package/core/frontend/helpers/get.js +4 -0
  28. package/core/frontend/helpers/match.js +12 -0
  29. package/core/frontend/helpers/prev_post.js +11 -1
  30. package/core/frontend/helpers/tiers.js +59 -0
  31. package/core/frontend/helpers/tpl/content-cta.hbs +1 -1
  32. package/core/frontend/public/ghost.css +205 -143
  33. package/core/frontend/services/routing/router-manager.js +1 -1
  34. package/core/frontend/views/unsubscribe.hbs +28 -33
  35. package/core/frontend/web/middleware/error-handler.js +2 -2
  36. package/core/frontend/web/site.js +10 -0
  37. package/core/server/api/canary/authentication.js +7 -0
  38. package/core/server/api/canary/index.js +4 -0
  39. package/core/server/api/canary/members.js +9 -2
  40. package/core/server/api/canary/products.js +3 -6
  41. package/core/server/api/canary/tiers-public.js +34 -0
  42. package/core/server/api/canary/tiers.js +6 -7
  43. package/core/server/api/canary/utils/serializers/input/pages.js +1 -1
  44. package/core/server/api/canary/utils/serializers/input/posts.js +1 -1
  45. package/core/server/api/canary/utils/serializers/output/email-posts.js +7 -1
  46. package/core/server/api/canary/utils/serializers/output/pages.js +9 -2
  47. package/core/server/api/canary/utils/serializers/output/posts.js +8 -2
  48. package/core/server/api/canary/utils/serializers/output/preview.js +7 -1
  49. package/core/server/api/canary/utils/serializers/output/products.js +3 -1
  50. package/core/server/api/canary/utils/serializers/output/tiers.js +4 -2
  51. package/core/server/api/canary/utils/serializers/output/utils/mapper.js +17 -7
  52. package/core/server/api/shared/serializers/handle.js +2 -2
  53. package/core/server/api/v2/utils/serializers/input/pages.js +1 -1
  54. package/core/server/api/v2/utils/serializers/input/posts.js +1 -1
  55. package/core/server/api/v3/utils/serializers/input/pages.js +1 -1
  56. package/core/server/api/v3/utils/serializers/input/posts.js +1 -1
  57. package/core/server/data/db/connection.js +3 -2
  58. package/core/server/data/importer/import-manager.js +152 -113
  59. package/core/server/data/migrations/versions/3.29/01-remove-duplicate-subscriptions.js +2 -1
  60. package/core/server/data/migrations/versions/3.29/02-remove-duplicate-customers.js +2 -1
  61. package/core/server/data/migrations/versions/3.29/03-remove-orphaned-customers.js +2 -1
  62. package/core/server/data/migrations/versions/3.29/04-remove-orphaned-subscriptions.js +2 -1
  63. package/core/server/data/migrations/versions/3.29/05-add-member-constraints.js +3 -2
  64. package/core/server/data/migrations/versions/3.39/06-add-email-recipient-index.js +4 -3
  65. package/core/server/data/migrations/versions/4.0/14-remove-orphaned-stripe-records.js +2 -1
  66. package/core/server/data/migrations/versions/4.0/26-add-cascade-on-delete.js +2 -1
  67. package/core/server/data/migrations/versions/4.0/29-fix-foreign-key-for-members-stripe-customers-subscriptions.js +2 -1
  68. package/core/server/data/migrations/versions/4.1/02-add-unique-constraint-for-member-stripe-tables.js +2 -1
  69. package/core/server/data/migrations/versions/4.20/05-remove-not-null-constraint-from-portal-title.js +3 -2
  70. package/core/server/data/migrations/versions/4.33/2022-01-14-11-51-add-default-free-tier.js +3 -0
  71. package/core/server/data/migrations/versions/4.33/2022-01-18-09-07-remove-duplicate-offer-redemptions.js +2 -2
  72. package/core/server/data/migrations/versions/4.35/2022-02-01-11-48-update-email-recipient-filter-column-type.js +2 -1
  73. package/core/server/data/migrations/versions/4.35/2022-02-01-12-03-update-recipient-filter-column-type.js +2 -1
  74. package/core/server/data/migrations/versions/4.37/2022-02-21-09-53-backfill-members-last-seen-at-column.js +3 -2
  75. package/core/server/data/migrations/versions/4.38/2022-03-01-08-46-add-visibility-to-tiers.js +11 -0
  76. package/core/server/data/migrations/versions/4.38/2022-03-03-16-12-add-visibility-to-tiers.js +8 -0
  77. package/core/server/data/migrations/versions/4.38/2022-03-03-16-17-drop-tiers-visible-column.js +7 -0
  78. package/core/server/data/migrations/versions/4.39/2022-03-07-10-57-update-free-products-visibility-column.js +66 -0
  79. package/core/server/data/migrations/versions/4.39/2022-03-07-10-57-update-products-visibility-column.js +36 -0
  80. package/core/server/data/schema/clients/index.js +1 -1
  81. package/core/server/data/schema/clients/mysql.js +4 -4
  82. package/core/server/data/schema/commands.js +42 -50
  83. package/core/server/data/schema/default-settings/default-settings.json +2 -2
  84. package/core/server/data/schema/fixtures/fixtures.json +18 -161
  85. package/core/server/data/schema/schema.js +7 -0
  86. package/core/server/frontend/ghost.min.css +1 -1
  87. package/core/server/lib/image/image-size.js +12 -4
  88. package/core/server/models/base/plugins/generate-slug.js +13 -1
  89. package/core/server/models/base/plugins/raw-knex.js +1 -1
  90. package/core/server/models/post.js +16 -6
  91. package/core/server/models/product.js +2 -1
  92. package/core/server/models/user.js +1 -1
  93. package/core/server/services/auth/api-key/admin.js +15 -6
  94. package/core/server/services/auth/setup.js +34 -13
  95. package/core/server/services/email-analytics/lib/event-processor.js +18 -1
  96. package/core/server/services/mega/mega.js +4 -4
  97. package/core/server/services/mega/template.js +1 -1
  98. package/core/server/services/members/content-gating.js +1 -1
  99. package/core/server/services/members/middleware.js +4 -0
  100. package/core/server/services/members/service.js +13 -1
  101. package/core/server/services/posts/posts-service.js +1 -1
  102. package/core/server/services/url/UrlGenerator.js +1 -1
  103. package/core/server/services/webhooks/webhooks-service.js +2 -0
  104. package/core/server/views/maintenance.html +2 -2
  105. package/core/server/web/admin/views/default-prod.html +4 -4
  106. package/core/server/web/admin/views/default.html +4 -4
  107. package/core/server/web/api/app.js +3 -0
  108. package/core/server/web/api/canary/admin/middleware.js +2 -0
  109. package/core/server/web/api/canary/content/routes.js +1 -0
  110. package/core/server/web/members/app.js +1 -1
  111. package/core/server/web/parent/backend.js +2 -1
  112. package/core/server/web/shared/middleware/uncapitalise.js +3 -2
  113. package/core/shared/config/defaults.json +2 -2
  114. package/core/shared/config/utils.js +5 -1
  115. package/core/shared/labs.js +8 -9
  116. package/core/shared/url-utils.js +4 -1
  117. package/package.json +56 -52
  118. package/yarn.lock +809 -607
  119. package/core/built/assets/ghost-dark-d54723f7267e66fa2595f897076e86c2.css +0 -1
  120. package/core/built/assets/ghost.min-02a5f8954bd85fe28817b8c8b111b8aa.css +0 -1
  121. package/core/built/assets/icons/event-started-subscription.svg +0 -6
  122. package/core/built/assets/icons/locked-email-back.svg +0 -1
  123. package/core/built/assets/icons/locked-email-front.svg +0 -1
  124. package/core/built/assets/icons/locked-email-lock.svg +0 -1
  125. package/core/built/assets/img/ghost-logo-de2acf283f53ba1fd1149928faeaaa74.png +0 -0
@@ -1,12 +1,10 @@
1
1
  const _ = require('lodash');
2
- const Promise = require('bluebird');
3
2
  const fs = require('fs-extra');
4
3
  const path = require('path');
5
4
  const os = require('os');
6
5
  const glob = require('glob');
7
6
  const uuid = require('uuid');
8
7
  const {extract} = require('@tryghost/zip');
9
- const {pipeline, sequence} = require('@tryghost/promise');
10
8
  const tpl = require('@tryghost/tpl');
11
9
  const logging = require('@tryghost/logging');
12
10
  const errors = require('@tryghost/errors');
@@ -21,7 +19,6 @@ const messages = {
21
19
  error: 'Import could not clean up file ',
22
20
  context: 'Your site will continue to work as expected'
23
21
  },
24
- unsupportedRoonExport: 'Your zip file looks like an old format Roon export, please re-export your Roon blog and try again.',
25
22
  noContentToImport: 'Zip did not include any content to import.',
26
23
  invalidZipStructure: 'Invalid zip file structure.',
27
24
  invalidZipFileBaseDirectory: 'Invalid zip file: base directory read failed',
@@ -41,10 +38,20 @@ let defaults = {
41
38
 
42
39
  class ImportManager {
43
40
  constructor() {
41
+ /**
42
+ * @type {Importer[]} importers
43
+ */
44
44
  this.importers = [ImageImporter, DataImporter];
45
+
46
+ /**
47
+ * @type {Handler[]}
48
+ */
45
49
  this.handlers = [ImageHandler, JSONHandler, MarkdownHandler];
46
50
 
47
51
  // Keep track of file to cleanup at the end
52
+ /**
53
+ * @type {?string}
54
+ */
48
55
  this.fileToDelete = null;
49
56
  }
50
57
 
@@ -53,7 +60,7 @@ class ImportManager {
53
60
  * @returns {string[]}
54
61
  */
55
62
  getExtensions() {
56
- return _.flatten(_.union(_.map(this.handlers, 'extensions'), defaults.extensions));
63
+ return _.union(_.flatMap(this.handlers, 'extensions'), defaults.extensions);
57
64
  }
58
65
 
59
66
  /**
@@ -61,7 +68,7 @@ class ImportManager {
61
68
  * @returns {string[]}
62
69
  */
63
70
  getContentTypes() {
64
- return _.flatten(_.union(_.map(this.handlers, 'contentTypes'), defaults.contentTypes));
71
+ return _.union(_.flatMap(this.handlers, 'contentTypes'), defaults.contentTypes);
65
72
  }
66
73
 
67
74
  /**
@@ -69,7 +76,7 @@ class ImportManager {
69
76
  * @returns {string[]}
70
77
  */
71
78
  getDirectories() {
72
- return _.flatten(_.union(_.map(this.handlers, 'directories'), defaults.directories));
79
+ return _.union(_.flatMap(this.handlers, 'directories'), defaults.directories);
73
80
  }
74
81
 
75
82
  /**
@@ -85,7 +92,7 @@ class ImportManager {
85
92
 
86
93
  /**
87
94
  * @param {String[]} extensions
88
- * @param {Number} level
95
+ * @param {Number} [level]
89
96
  * @returns {String}
90
97
  */
91
98
  getExtensionGlob(extensions, level) {
@@ -98,7 +105,7 @@ class ImportManager {
98
105
  /**
99
106
  *
100
107
  * @param {String[]} directories
101
- * @param {Number} level
108
+ * @param {Number} [level]
102
109
  * @returns {String}
103
110
  */
104
111
  getDirectoryGlob(directories, level) {
@@ -110,26 +117,24 @@ class ImportManager {
110
117
 
111
118
  /**
112
119
  * Remove files after we're done (abstracted into a function for easier testing)
113
- * @returns {Function}
120
+ * @returns {Promise<void>}
114
121
  */
115
- cleanUp() {
116
- const self = this;
117
-
118
- if (self.fileToDelete === null) {
122
+ async cleanUp() {
123
+ if (this.fileToDelete === null) {
119
124
  return;
120
125
  }
121
126
 
122
- fs.remove(self.fileToDelete, function (err) {
123
- if (err) {
124
- logging.error(new errors.InternalServerError({
125
- err: err,
126
- context: tpl(messages.couldNotCleanUpFile.error),
127
- help: tpl(messages.couldNotCleanUpFile.context)
128
- }));
129
- }
127
+ try {
128
+ await fs.remove(this.fileToDelete);
129
+ } catch (err) {
130
+ logging.error(new errors.InternalServerError({
131
+ err: err,
132
+ context: tpl(messages.couldNotCleanUpFile.error),
133
+ help: tpl(messages.couldNotCleanUpFile.context)
134
+ }));
135
+ }
130
136
 
131
- self.fileToDelete = null;
132
- });
137
+ this.fileToDelete = null;
133
138
  }
134
139
 
135
140
  /**
@@ -146,28 +151,20 @@ class ImportManager {
146
151
  * Importable content must be found either in the root, or inside one base directory
147
152
  *
148
153
  * @param {String} directory
149
- * @returns {Promise}
154
+ * @returns {boolean}
150
155
  */
151
156
  isValidZip(directory) {
152
157
  // Globs match content in the root or inside a single directory
153
- const extMatchesBase = glob.sync(this.getExtensionGlob(this.getExtensions(), ROOT_OR_SINGLE_DIR), {cwd: directory});
158
+ const extMatchesBase = glob.sync(this.getExtensionGlob(this.getExtensions(), ROOT_OR_SINGLE_DIR), {cwd: directory, nocase: true});
154
159
 
155
160
  const extMatchesAll = glob.sync(
156
- this.getExtensionGlob(this.getExtensions(), ALL_DIRS), {cwd: directory}
161
+ this.getExtensionGlob(this.getExtensions(), ALL_DIRS), {cwd: directory, nocase: true}
157
162
  );
158
163
 
159
164
  const dirMatches = glob.sync(
160
165
  this.getDirectoryGlob(this.getDirectories(), ROOT_OR_SINGLE_DIR), {cwd: directory}
161
166
  );
162
167
 
163
- const oldRoonMatches = glob.sync(this.getDirectoryGlob(['drafts', 'published', 'deleted'], ROOT_OR_SINGLE_DIR),
164
- {cwd: directory});
165
-
166
- // This is a temporary extra message for the old format roon export which doesn't work with Ghost
167
- if (oldRoonMatches.length > 0) {
168
- throw new errors.UnsupportedMediaTypeError({message: tpl(messages.unsupportedRoonExport)});
169
- }
170
-
171
168
  // If this folder contains importable files or a content or images directory
172
169
  if (extMatchesBase.length > 0 || (dirMatches.length > 0 && extMatchesAll.length > 0)) {
173
170
  return true;
@@ -182,8 +179,8 @@ class ImportManager {
182
179
 
183
180
  /**
184
181
  * Use the extract module to extract the given zip file to a temp directory & return the temp directory path
185
- * @param {String} filePath
186
- * @returns {Promise[]} Files
182
+ * @param {string} filePath
183
+ * @returns {Promise<string>} full path to the extracted folder
187
184
  */
188
185
  extractZip(filePath) {
189
186
  const tmpDir = path.join(os.tmpdir(), uuid.v4());
@@ -199,11 +196,11 @@ class ImportManager {
199
196
  * are relevant to the given handler, and return them as a name and path combo
200
197
  * @param {Object} handler
201
198
  * @param {String} directory
202
- * @returns [] Files
199
+ * @returns {File[]} Files
203
200
  */
204
201
  getFilesFromZip(handler, directory) {
205
202
  const globPattern = this.getExtensionGlob(handler.extensions, ALL_DIRS);
206
- return _.map(glob.sync(globPattern, {cwd: directory}), function (file) {
203
+ return _.map(glob.sync(globPattern, {cwd: directory, nocase: true}), function (file) {
207
204
  return {name: file, path: path.join(directory, file)};
208
205
  });
209
206
  }
@@ -215,9 +212,9 @@ class ImportManager {
215
212
  */
216
213
  getBaseDirectory(directory) {
217
214
  // Globs match root level only
218
- const extMatches = glob.sync(this.getExtensionGlob(this.getExtensions(), ROOT_ONLY), {cwd: directory});
215
+ const extMatches = glob.sync(this.getExtensionGlob(this.getExtensions(), ROOT_ONLY), {cwd: directory, nocase: true});
219
216
 
220
- const dirMatches = glob.sync(this.getDirectoryGlob(this.getDirectories(), ROOT_ONLY), {cwd: directory});
217
+ const dirMatches = glob.sync(this.getDirectoryGlob(this.getDirectories(), ROOT_ONLY), {cwd: directory, nocase: true});
221
218
  let extMatchesAll;
222
219
 
223
220
  // There is no base directory
@@ -226,9 +223,9 @@ class ImportManager {
226
223
  }
227
224
  // There is a base directory, grab it from any ext match
228
225
  extMatchesAll = glob.sync(
229
- this.getExtensionGlob(this.getExtensions(), ALL_DIRS), {cwd: directory}
226
+ this.getExtensionGlob(this.getExtensions(), ALL_DIRS), {cwd: directory, nocase: true}
230
227
  );
231
- if (extMatchesAll.length < 1 || extMatchesAll[0].split('/') < 1) {
228
+ if (extMatchesAll.length < 1 || extMatchesAll[0].split('/').length < 1) {
232
229
  throw new errors.ValidationError({message: tpl(messages.invalidZipFileBaseDirectory)});
233
230
  }
234
231
 
@@ -242,48 +239,42 @@ class ImportManager {
242
239
  * The data key contains JSON representing any data that should be imported
243
240
  * The image key contains references to images that will be stored (and where they will be stored)
244
241
  * @param {File} file
245
- * @returns {Promise(ImportData)}
242
+ * @returns {Promise<ImportData>}
246
243
  */
247
- processZip(file) {
248
- const self = this;
244
+ async processZip(file) {
245
+ const zipDirectory = await this.extractZip(file.path);
249
246
 
250
- return this.extractZip(file.path).then(function (zipDirectory) {
251
- const ops = [];
252
- const importData = {};
253
- let baseDir;
247
+ /**
248
+ * @type {ImportData}
249
+ */
250
+ const importData = {};
251
+
252
+ this.isValidZip(zipDirectory);
253
+ const baseDir = this.getBaseDirectory(zipDirectory);
254
254
 
255
- self.isValidZip(zipDirectory);
256
- baseDir = self.getBaseDirectory(zipDirectory);
255
+ for (const handler of this.handlers) {
256
+ const files = this.getFilesFromZip(handler, zipDirectory);
257
257
 
258
- _.each(self.handlers, function (handler) {
258
+ if (files.length > 0) {
259
259
  if (Object.prototype.hasOwnProperty.call(importData, handler.type)) {
260
260
  // This limitation is here to reduce the complexity of the importer for now
261
- return Promise.reject(new errors.UnsupportedMediaTypeError({
261
+ throw new errors.UnsupportedMediaTypeError({
262
262
  message: tpl(messages.zipContainsMultipleDataFormats)
263
- }));
264
- }
265
-
266
- const files = self.getFilesFromZip(handler, zipDirectory);
267
-
268
- if (files.length > 0) {
269
- ops.push(function () {
270
- return handler.loadFile(files, baseDir).then(function (data) {
271
- importData[handler.type] = data;
272
- });
273
263
  });
274
264
  }
275
- });
276
265
 
277
- if (ops.length === 0) {
278
- return Promise.reject(new errors.UnsupportedMediaTypeError({
279
- message: tpl(messages.noContentToImport)
280
- }));
266
+ const data = await handler.loadFile(files, baseDir);
267
+ importData[handler.type] = data;
281
268
  }
269
+ }
282
270
 
283
- return sequence(ops).then(function () {
284
- return importData;
271
+ if (Object.keys(importData).length === 0) {
272
+ throw new errors.UnsupportedMediaTypeError({
273
+ message: tpl(messages.noContentToImport)
285
274
  });
286
- });
275
+ }
276
+
277
+ return importData;
287
278
  }
288
279
 
289
280
  /**
@@ -293,7 +284,7 @@ class ImportManager {
293
284
  * The data key contains JSON representing any data that should be imported
294
285
  * The image key contains references to images that will be stored (and where they will be stored)
295
286
  * @param {File} file
296
- * @returns {Promise(ImportData)}
287
+ * @returns {Promise<ImportData>}
297
288
  */
298
289
  processFile(file, ext) {
299
290
  const fileHandler = _.find(this.handlers, function (handler) {
@@ -313,7 +304,7 @@ class ImportManager {
313
304
  * Load the given file into usable importData in the format: {data: {}, images: []}, regardless of
314
305
  * whether the file is a single importable file like a JSON file, or a zip file containing loads of files.
315
306
  * @param {File} file
316
- * @returns {Promise}
307
+ * @returns {Promise<ImportData>}
317
308
  */
318
309
  loadFile(file) {
319
310
  const self = this;
@@ -326,17 +317,14 @@ class ImportManager {
326
317
  * Pass the prepared importData through the preProcess function of the various importers, so that the importers can
327
318
  * make any adjustments to the data based on relationships between it
328
319
  * @param {ImportData} importData
329
- * @returns {Promise(ImportData)}
320
+ * @returns {Promise<ImportData>}
330
321
  */
331
- preProcess(importData) {
332
- const ops = [];
333
- _.each(this.importers, function (importer) {
334
- ops.push(function () {
335
- return importer.preProcess(importData);
336
- });
337
- });
322
+ async preProcess(importData) {
323
+ for (const importer of this.importers) {
324
+ importData = importer.preProcess(importData);
325
+ }
338
326
 
339
- return pipeline(ops);
327
+ return Promise.resolve(importData);
340
328
  }
341
329
 
342
330
  /**
@@ -344,65 +332,116 @@ class ImportManager {
344
332
  * Each importer gets passed the data from importData which has the key matching its type - i.e. it only gets the
345
333
  * data that it should import. Each importer then handles actually importing that data into Ghost
346
334
  * @param {ImportData} importData
347
- * @param {Object} importOptions to allow override of certain import features such as locking a user
348
- * @returns {Promise<any>}
335
+ * @param {ImportOptions} [importOptions] to allow override of certain import features such as locking a user
336
+ * @returns {Promise<ImportResult[]>} importResults
349
337
  */
350
- doImport(importData, importOptions) {
338
+ async doImport(importData, importOptions) {
351
339
  importOptions = importOptions || {};
352
- const ops = [];
353
- _.each(this.importers, function (importer) {
340
+ const importResults = [];
341
+
342
+ for (const importer of this.importers) {
354
343
  if (Object.prototype.hasOwnProperty.call(importData, importer.type)) {
355
- ops.push(function () {
356
- return importer.doImport(importData[importer.type], importOptions);
357
- });
344
+ importResults.push(await importer.doImport(importData[importer.type], importOptions));
358
345
  }
359
- });
346
+ }
360
347
 
361
- return sequence(ops).then(function (importResult) {
362
- return importResult;
363
- });
348
+ return importResults;
364
349
  }
365
350
 
366
351
  /**
367
352
  * Import Step 4:
368
353
  * Report on what was imported, currently a no-op
369
- * @param {ImportData} importData
370
- * @returns {Promise<ImportData>}
354
+ * @param {ImportResult[]} importResults
355
+ * @returns {Promise<ImportResult[]>} importResults
371
356
  */
372
- generateReport(importData) {
373
- return Promise.resolve(importData);
357
+ async generateReport(importResults) {
358
+ return Promise.resolve(importResults);
374
359
  }
375
360
 
376
361
  /**
377
362
  * Import From File
378
363
  * The main method of the ImportManager, call this to kick everything off!
379
364
  * @param {File} file
380
- * @param {Object} importOptions to allow override of certain import features such as locking a user
381
- * @returns {Promise}
365
+ * @param {ImportOptions} importOptions to allow override of certain import features such as locking a user
366
+ * @returns {Promise<ImportResult[]>}
382
367
  */
383
- importFromFile(file, importOptions = {}) {
384
- const self = this;
368
+ async importFromFile(file, importOptions = {}) {
369
+ try {
370
+ // Step 1: Handle converting the file to usable data
371
+ let importData = await this.loadFile(file);
385
372
 
386
- // Step 1: Handle converting the file to usable data
387
- return this.loadFile(file).then(function (importData) {
388
373
  // Step 2: Let the importers pre-process the data
389
- return self.preProcess(importData);
390
- }).then(function (importData) {
374
+ importData = await this.preProcess(importData);
375
+
391
376
  // Step 3: Actually do the import
392
377
  // @TODO: It would be cool to have some sort of dry run flag here
393
- return self.doImport(importData, importOptions);
394
- }).then(function (importData) {
378
+ let importResult = await this.doImport(importData, importOptions);
379
+
395
380
  // Step 4: Report on the import
396
- return self.generateReport(importData);
397
- }).finally(() => self.cleanUp()); // Step 5: Cleanup any files
381
+ return await this.generateReport(importResult);
382
+ } finally {
383
+ // Step 5: Cleanup any files
384
+ this.cleanUp();
385
+ }
398
386
  }
399
387
  }
400
388
 
401
389
  /**
402
- * A number, or a string containing a number.
390
+ * @typedef {object} ImportOptions
391
+ * @property {boolean} [returnImportedData]
392
+ * @property {boolean} [importPersistUser]
393
+ */
394
+
395
+ /**
396
+ * @typedef {object} Importer
397
+ * @property {"images"|"data"} type
398
+ * @property {PreProcessMethod} preProcess
399
+ * @property {DoImportMethod} doImport
400
+ */
401
+
402
+ /**
403
+ * @callback PreProcessMethod
404
+ * @param {ImportData} importData
405
+ * @returns {ImportData}
406
+ */
407
+
408
+ /**
409
+ * @callback DoImportMethod
410
+ * @param {object|object[]} importData
411
+ * @param {ImportOptions} importOptions
412
+ * @returns {Promise<ImportResult>} import result
413
+ */
414
+
415
+ /**
416
+ * @typedef {object} Handler
417
+ * @property {"images"|"data"} type
418
+ * @property {string[]} extensions
419
+ * @property {string[]} contentTypes
420
+ * @property {string[]} directories
421
+ * @property {LoadFileMethod} loadFile
422
+ */
423
+
424
+ /**
425
+ * @callback LoadFileMethod
426
+ * @param {File[]} files
427
+ * @param {string} [baseDir]
428
+ * @returns {Promise<object[]|object>} data
429
+ */
430
+
431
+ /**
432
+ * File object
433
+ * @typedef {Object} File
434
+ * @property {string} name
435
+ * @property {string} path
436
+ */
437
+
438
+ /**
403
439
  * @typedef {Object} ImportData
404
- * @property [Object] data
405
- * @property [Array] images
440
+ * @property {Object} [data]
441
+ * @property {Array} [images]
406
442
  */
407
443
 
444
+ /**
445
+ * @typedef {Object} ImportResult
446
+ */
408
447
  module.exports = new ImportManager();
@@ -1,4 +1,5 @@
1
1
  const logging = require('@tryghost/logging');
2
+ const DatabaseInfo = require('@tryghost/database-info');
2
3
 
3
4
  module.exports = {
4
5
  config: {
@@ -6,7 +7,7 @@ module.exports = {
6
7
  },
7
8
 
8
9
  async up({transacting: knex}) {
9
- if (knex.client.config.client !== 'mysql') {
10
+ if (!DatabaseInfo.isMySQL(knex)) {
10
11
  logging.warn('Skipping cleanup of duplicate subscriptions - database is not MySQL');
11
12
  return;
12
13
  }
@@ -1,4 +1,5 @@
1
1
  const logging = require('@tryghost/logging');
2
+ const DatabaseInfo = require('@tryghost/database-info');
2
3
 
3
4
  module.exports = {
4
5
  config: {
@@ -6,7 +7,7 @@ module.exports = {
6
7
  },
7
8
 
8
9
  async up({transacting: knex}) {
9
- if (knex.client.config.client !== 'mysql') {
10
+ if (!DatabaseInfo.isMySQL(knex)) {
10
11
  logging.warn('Skipping cleanup of duplicate customers - database is not MySQL');
11
12
  return;
12
13
  }
@@ -1,4 +1,5 @@
1
1
  const logging = require('@tryghost/logging');
2
+ const DatabaseInfo = require('@tryghost/database-info');
2
3
 
3
4
  module.exports = {
4
5
  config: {
@@ -6,7 +7,7 @@ module.exports = {
6
7
  },
7
8
 
8
9
  async up({transacting: knex}) {
9
- if (knex.client.config.client !== 'mysql') {
10
+ if (!DatabaseInfo.isMySQL(knex)) {
10
11
  logging.warn('Skipping cleanup of orphaned customers - database is not MySQL');
11
12
  return;
12
13
  }
@@ -1,4 +1,5 @@
1
1
  const logging = require('@tryghost/logging');
2
+ const DatabaseInfo = require('@tryghost/database-info');
2
3
 
3
4
  module.exports = {
4
5
  config: {
@@ -6,7 +7,7 @@ module.exports = {
6
7
  },
7
8
 
8
9
  async up({transacting: knex}) {
9
- if (knex.client.config.client !== 'mysql') {
10
+ if (!DatabaseInfo.isMySQL(knex)) {
10
11
  logging.warn('Skipping cleanup of orphaned subscriptions - database is not MySQL');
11
12
  return;
12
13
  }
@@ -1,4 +1,5 @@
1
1
  const logging = require('@tryghost/logging');
2
+ const DatabaseInfo = require('@tryghost/database-info');
2
3
 
3
4
  module.exports = {
4
5
  config: {
@@ -6,7 +7,7 @@ module.exports = {
6
7
  },
7
8
 
8
9
  async up({transacting: knex}) {
9
- if (knex.client.config.client !== 'mysql') {
10
+ if (!DatabaseInfo.isMySQL(knex)) {
10
11
  return logging.warn('Skipping member tables index creation - database is not MySQL');
11
12
  }
12
13
 
@@ -91,7 +92,7 @@ module.exports = {
91
92
  },
92
93
 
93
94
  async down({transacting: knex}) {
94
- if (knex.client.config.client !== 'mysql') {
95
+ if (!DatabaseInfo.isMySQL(knex)) {
95
96
  return logging.warn('Skipping member tables index removal - database is not MySQL');
96
97
  }
97
98
 
@@ -1,11 +1,12 @@
1
1
  const logging = require('@tryghost/logging');
2
2
  const {createNonTransactionalMigration} = require('../../utils');
3
+ const DatabaseInfo = require('@tryghost/database-info');
3
4
 
4
5
  module.exports = createNonTransactionalMigration(
5
6
  async function up(knex) {
6
7
  let hasIndex = false;
7
8
 
8
- if (knex.client.config.client === 'sqlite3') {
9
+ if (DatabaseInfo.isSQLite(knex)) {
9
10
  const result = await knex.raw(`select * from sqlite_master where type = 'index' and tbl_name = 'email_recipients' and name = 'email_recipients_email_id_member_email_index'`);
10
11
  hasIndex = result.length !== 0;
11
12
  } else {
@@ -27,7 +28,7 @@ module.exports = createNonTransactionalMigration(
27
28
  async function down(knex) {
28
29
  let missingIndex = false;
29
30
 
30
- if (knex.client.config.client === 'sqlite3') {
31
+ if (DatabaseInfo.isSQLite(knex)) {
31
32
  const result = await knex.raw(`select * from sqlite_master where type = 'index' and tbl_name = 'email_recipients' and name = 'email_recipients_email_id_member_email_index'`);
32
33
  missingIndex = result.length === 0;
33
34
  } else {
@@ -42,7 +43,7 @@ module.exports = createNonTransactionalMigration(
42
43
 
43
44
  logging.info('Dropping composite index on email_recipients for [email_id, member_email]');
44
45
 
45
- if (knex.client.config.client === 'mysql') {
46
+ if (DatabaseInfo.isMySQL(knex)) {
46
47
  await knex.schema.table('email_recipients', (table) => {
47
48
  table.dropForeign('email_id');
48
49
  table.dropIndex(['email_id', 'member_email']);
@@ -1,8 +1,9 @@
1
1
  const {createIrreversibleMigration} = require('../../utils');
2
2
  const logging = require('@tryghost/logging');
3
+ const DatabaseInfo = require('@tryghost/database-info');
3
4
 
4
5
  module.exports = createIrreversibleMigration(async function up(connection) {
5
- if (connection.client.config.client === 'mysql') {
6
+ if (DatabaseInfo.isMySQL(connection)) {
6
7
  logging.info('Skipping removal of orphaned stripe records for MySQL');
7
8
  return;
8
9
  }
@@ -1,9 +1,10 @@
1
1
  const logging = require('@tryghost/logging');
2
2
  const {createIrreversibleMigration} = require('../../utils');
3
3
  const {addForeign, dropForeign} = require('../../../schema/commands');
4
+ const DatabaseInfo = require('@tryghost/database-info');
4
5
 
5
6
  module.exports = createIrreversibleMigration(async (knex) => {
6
- if (knex.client.config.client !== 'sqlite3') {
7
+ if (!DatabaseInfo.isSQLite(knex)) {
7
8
  return logging.warn('Skipping adding "on delete cascade" - database is not SQLite3');
8
9
  }
9
10
 
@@ -1,9 +1,10 @@
1
1
  const logging = require('@tryghost/logging');
2
2
  const {createIrreversibleMigration} = require('../../utils');
3
3
  const {addForeign, dropForeign} = require('../../../schema/commands');
4
+ const DatabaseInfo = require('@tryghost/database-info');
4
5
 
5
6
  module.exports = createIrreversibleMigration(async (knex) => {
6
- if (knex.client.config.client !== 'sqlite3') {
7
+ if (!DatabaseInfo.isSQLite(knex)) {
7
8
  return logging.warn('Skipping fixing foreign key for members_stripe_customers_subscriptions - database is not SQLite3');
8
9
  }
9
10
 
@@ -1,10 +1,11 @@
1
1
  const logging = require('@tryghost/logging');
2
2
  const {createTransactionalMigration} = require('../../utils');
3
3
  const {addUnique} = require('../../../schema/commands');
4
+ const DatabaseInfo = require('@tryghost/database-info');
4
5
 
5
6
  module.exports = createTransactionalMigration(
6
7
  async function up(connection) {
7
- if (connection.client.config.client !== 'sqlite3') {
8
+ if (!DatabaseInfo.isSQLite(connection)) {
8
9
  return logging.warn('Skipping adding unique constraint for members_stripe_customers_subscriptions and members_stripe_customers - database is not SQLite3');
9
10
  }
10
11
 
@@ -1,6 +1,7 @@
1
1
  const logging = require('@tryghost/logging');
2
2
  const {createNonTransactionalMigration} = require('../../utils');
3
3
  const {addUnique} = require('../../../schema/commands');
4
+ const DatabaseInfo = require('@tryghost/database-info');
4
5
 
5
6
  module.exports = createNonTransactionalMigration(
6
7
  async function up(knex) {
@@ -14,7 +15,7 @@ module.exports = createNonTransactionalMigration(
14
15
  table.string('portal_title', 191).nullable();
15
16
  });
16
17
 
17
- if (knex.client.config.client === 'sqlite3') {
18
+ if (DatabaseInfo.isSQLite(knex)) {
18
19
  // eslint-disable-next-line no-restricted-syntax
19
20
  for (const column of ['name', 'code', 'stripe_coupon_id']) {
20
21
  await addUnique('offers', column, knex);
@@ -32,7 +33,7 @@ module.exports = createNonTransactionalMigration(
32
33
  table.string('portal_title', 191).notNullable();
33
34
  });
34
35
 
35
- if (knex.client.config.client === 'sqlite3') {
36
+ if (DatabaseInfo.isSQLite(knex)) {
36
37
  // eslint-disable-next-line no-restricted-syntax
37
38
  for (const column of ['name', 'code', 'stripe_coupon_id']) {
38
39
  await addUnique('offers', column, knex);