apostrophe 3.4.1 → 3.8.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 (111) hide show
  1. package/.eslintrc +4 -0
  2. package/.scratch.md +2 -0
  3. package/CHANGELOG.md +114 -2
  4. package/README.md +1 -1
  5. package/deploy-test-count +1 -1
  6. package/index.js +125 -5
  7. package/lib/moog-require.js +41 -3
  8. package/lib/moog.js +20 -8
  9. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarLocale.vue +42 -23
  10. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +30 -14
  11. package/modules/@apostrophecms/area/index.js +9 -0
  12. package/modules/@apostrophecms/area/lib/custom-tags/area.js +1 -1
  13. package/modules/@apostrophecms/area/lib/custom-tags/widget.js +1 -1
  14. package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +3 -0
  15. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +6 -6
  16. package/modules/@apostrophecms/asset/index.js +85 -21
  17. package/modules/@apostrophecms/asset/lib/globalIcons.js +2 -0
  18. package/modules/@apostrophecms/asset/lib/webpack/src/webpack.scss.js +5 -2
  19. package/modules/@apostrophecms/attachment/index.js +1 -0
  20. package/modules/@apostrophecms/db/index.js +5 -6
  21. package/modules/@apostrophecms/doc/index.js +13 -3
  22. package/modules/@apostrophecms/doc-type/index.js +24 -4
  23. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +13 -1
  24. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +3 -0
  25. package/modules/@apostrophecms/i18n/i18n/en.json +26 -6
  26. package/modules/@apostrophecms/i18n/i18n/es.json +382 -0
  27. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +379 -0
  28. package/modules/@apostrophecms/i18n/i18n/sk.json +380 -0
  29. package/modules/@apostrophecms/i18n/index.js +10 -1
  30. package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalize.vue +153 -121
  31. package/modules/@apostrophecms/image/index.js +2 -1
  32. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +6 -3
  33. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerEditor.vue +24 -13
  34. package/modules/@apostrophecms/image-widget/index.js +2 -1
  35. package/modules/@apostrophecms/image-widget/views/widget.html +12 -2
  36. package/modules/@apostrophecms/job/index.js +164 -212
  37. package/modules/@apostrophecms/login/index.js +36 -17
  38. package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +8 -0
  39. package/modules/@apostrophecms/migration/index.js +1 -1
  40. package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +151 -61
  41. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +6 -2
  42. package/modules/@apostrophecms/modal/ui/apos/components/AposModalConfirm.vue +9 -7
  43. package/modules/@apostrophecms/modal/ui/apos/mixins/AposDocsManagerMixin.js +12 -15
  44. package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +6 -0
  45. package/modules/@apostrophecms/module/index.js +1 -1
  46. package/modules/@apostrophecms/notification/index.js +116 -8
  47. package/modules/@apostrophecms/notification/ui/apos/components/AposNotification.vue +89 -11
  48. package/modules/@apostrophecms/notification/ui/apos/components/TheAposNotifications.vue +1 -1
  49. package/modules/@apostrophecms/page/index.js +37 -30
  50. package/modules/@apostrophecms/permission/index.js +1 -1
  51. package/modules/@apostrophecms/permission/ui/apos/components/AposInputRole.vue +4 -2
  52. package/modules/@apostrophecms/piece-type/index.js +178 -61
  53. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +179 -47
  54. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerDisplay.vue +1 -3
  55. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerSelectBox.vue +138 -0
  56. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +42 -10
  57. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapStyles.vue +3 -0
  58. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Classes.js +6 -10
  59. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Default.js +64 -0
  60. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Document.js +15 -0
  61. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Heading.js +23 -0
  62. package/modules/@apostrophecms/schema/index.js +97 -20
  63. package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +1 -0
  64. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +4 -1
  65. package/modules/@apostrophecms/schema/ui/apos/components/AposInputAttachment.vue +11 -160
  66. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRadio.vue +8 -5
  67. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +24 -2
  68. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +24 -6
  69. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +0 -4
  70. package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +0 -7
  71. package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +25 -3
  72. package/modules/@apostrophecms/schema/ui/apos/mixins/AposInputMixin.js +10 -2
  73. package/modules/@apostrophecms/task/index.js +2 -2
  74. package/modules/@apostrophecms/template/index.js +63 -36
  75. package/modules/@apostrophecms/template/lib/custom-tags/component.js +1 -1
  76. package/modules/@apostrophecms/template/lib/custom-tags/render.js +6 -2
  77. package/modules/@apostrophecms/ui/index.js +6 -2
  78. package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +21 -3
  79. package/modules/@apostrophecms/ui/ui/apos/components/AposCellContextMenu.vue +1 -1
  80. package/modules/@apostrophecms/ui/ui/apos/components/AposFile.vue +205 -0
  81. package/modules/@apostrophecms/ui/ui/apos/components/AposIndicator.vue +5 -0
  82. package/modules/@apostrophecms/ui/ui/apos/lib/i18next.js +16 -2
  83. package/modules/@apostrophecms/ui/ui/apos/scss/global/_tables.scss +4 -3
  84. package/modules/@apostrophecms/ui/ui/apos/scss/global/_theme.scss +3 -0
  85. package/modules/@apostrophecms/ui/ui/apos/scss/global/_widgets.scss +3 -0
  86. package/modules/@apostrophecms/ui/ui/apos/scss/global/import-all.scss +2 -1
  87. package/modules/@apostrophecms/user/index.js +21 -0
  88. package/modules/@apostrophecms/util/index.js +2 -2
  89. package/modules/@apostrophecms/util/ui/src/http.js +12 -8
  90. package/modules/@apostrophecms/util/ui/src/util.js +15 -0
  91. package/modules/@apostrophecms/widget-type/index.js +1 -1
  92. package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +1 -0
  93. package/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js +15 -7
  94. package/package.json +4 -4
  95. package/test/extra_node_modules/improve-global/index.js +7 -0
  96. package/test/extra_node_modules/improve-piece-type/index.js +7 -0
  97. package/test/improve-overrides.js +30 -0
  98. package/test/job.js +224 -0
  99. package/test/login.js +183 -0
  100. package/test/modules/@apostrophecms/global/index.js +8 -0
  101. package/test/modules/fragment-all/views/aux-test.html +7 -0
  102. package/test/modules/fragment-all/views/fragment.html +5 -0
  103. package/test/moog.js +47 -0
  104. package/test/package.json +5 -4
  105. package/test/pieces.js +17 -0
  106. package/test/reverse-relationship.js +170 -0
  107. package/test/subdir-project/app.js +3 -0
  108. package/test/subdir-project.js +26 -0
  109. package/test/templates.js +7 -1
  110. package/test-lib/test.js +23 -12
  111. package/test-lib/util.js +33 -0
@@ -7,6 +7,7 @@
7
7
  <component
8
8
  :is="icon"
9
9
  :size="iconSize"
10
+ :title="title ? title : ''"
10
11
  class="apos-indicator__icon"
11
12
  :fill-color="iconColor"
12
13
  />
@@ -30,6 +31,10 @@ export default {
30
31
  type: [ String, Object, Boolean ],
31
32
  default: false
32
33
  },
34
+ title: {
35
+ type: [ String, Boolean ],
36
+ default: false
37
+ },
33
38
  iconColor: {
34
39
  type: String,
35
40
  default: 'currentColor'
@@ -10,11 +10,20 @@ export default {
10
10
  install(Vue, options) {
11
11
  const i18n = options.i18n;
12
12
 
13
+ const fallbackLng = [ i18n.defaultLocale ];
14
+ // In case the default locale also has inadequate admin UI phrases
15
+ if (fallbackLng[0] !== 'en') {
16
+ fallbackLng.push('en');
17
+ }
18
+
13
19
  i18next.init({
14
20
  lng: i18n.locale,
15
- fallbackLng: i18n.defaultLocale,
21
+ fallbackLng,
16
22
  resources: {},
17
- debug: i18n.debug
23
+ debug: i18n.debug,
24
+ interpolation: {
25
+ escapeValue: false
26
+ }
18
27
  });
19
28
 
20
29
  for (const [ ns, phrases ] of Object.entries(i18n.i18n[i18n.locale])) {
@@ -25,6 +34,11 @@ export default {
25
34
  i18next.addResourceBundle(i18n.defaultLocale, ns, phrases, true, true);
26
35
  }
27
36
  }
37
+ if ((i18n.locale !== 'en') && (i18n.defaultLocale !== 'en')) {
38
+ for (const [ ns, phrases ] of Object.entries(i18n.i18n.en)) {
39
+ i18next.addResourceBundle('en', ns, phrases, true, true);
40
+ }
41
+ }
28
42
 
29
43
  // Like standard i18next $t, but also with support
30
44
  // for just one object argument with at least a `key`
@@ -5,7 +5,7 @@
5
5
 
6
6
  .apos-table__header {
7
7
  margin-bottom: $spacing-base;
8
- padding: 12.5px 4.5px;
8
+ padding: 12.5px 15px;
9
9
  border-bottom: 1px solid var(--a-base-8);
10
10
  color: var(--a-base-3);
11
11
  text-align: left;
@@ -52,12 +52,13 @@ span.apos-table__header-label:hover {
52
52
  @include apos-transition(all, 0.05s);
53
53
  }
54
54
  .apos-table__cell {
55
- padding: 5px;
55
+ padding: 5px 15px;
56
56
  border-bottom: 1px solid var(--a-base-10);
57
57
  }
58
58
 
59
59
  .apos-table__cell--context-menu {
60
- width: 40px;
60
+ padding-right: 0;
61
+ padding-left: 0;
61
62
  }
62
63
 
63
64
  .apos-table__cell-field--context-menu {
@@ -78,6 +78,9 @@
78
78
  --a-line-base: 1;
79
79
  --a-line-tall: 1.3;
80
80
  --a-line-tallest: 1.6;
81
+
82
+ // Admin styles
83
+ --a-widget-margin: 0;
81
84
  }
82
85
 
83
86
  .apos-theme-dark {
@@ -0,0 +1,3 @@
1
+ [data-apos-area] [data-apos-area] {
2
+ margin: var(--a-widget-margin);
3
+ }
@@ -9,4 +9,5 @@
9
9
  @import './_scrollbars';
10
10
  @import './_utilities';
11
11
  @import './_tables';
12
- @import './_tooltips';
12
+ @import './_tooltips';
13
+ @import './_widgets';
@@ -185,6 +185,19 @@ module.exports = {
185
185
  if (![ 'guest', 'editor', 'contributor', 'admin' ].includes(doc.role)) {
186
186
  throw self.apos.error('invalid', 'The role property of a user must be guest, editor, contributor or admin');
187
187
  }
188
+ },
189
+ async invalidatePriorLogins(req, doc, options) {
190
+ const effectiveUserId = req.user && req.user._id;
191
+ // Invalidate prior login sessions if the password field changes or
192
+ // the user is newly marked as disabled.
193
+ if (doc._id && doc._passwordUpdated && (effectiveUserId !== doc._id)) {
194
+ // Invalidate old sessions
195
+ doc.loginInvalidBefore = Date.now();
196
+ // Just delete old bearer tokens
197
+ return self.apos.login.bearerTokens.removeMany({
198
+ userId: doc._id
199
+ });
200
+ }
188
201
  }
189
202
  },
190
203
  // Reflect email and username changes in the safe after deduplicating in the piece
@@ -347,6 +360,13 @@ module.exports = {
347
360
  // alone and `safeUser` is not updated.
348
361
  //
349
362
  // Called automatically by `hashSecrets`, above.
363
+ //
364
+ // The secret property itself is immediately deleted from doc
365
+ // to avoid any risk of accidentally storing it in cleartext.
366
+ // However there is a way to detect that it was updated:
367
+ // if `secret` is `password`, then the `_passwordUpdated` temporary
368
+ // property is set to true. This provides a way to take additional
369
+ // actions stemming from this change in a `beforeSave` handler, etc.
350
370
 
351
371
  async hashSecret(doc, safeUser, secret) {
352
372
  if (!doc[secret]) {
@@ -355,6 +375,7 @@ module.exports = {
355
375
  const hash = await require('util').promisify(self.pw.hash)(doc[secret]);
356
376
  delete doc[secret];
357
377
  safeUser[secret + 'Hash'] = hash;
378
+ doc[`_${secret}Updated`] = true;
358
379
  },
359
380
 
360
381
  // Verify the given password by checking it against the
@@ -414,7 +414,7 @@ module.exports = {
414
414
  return self.insensitiveSortCompare(a[property], b[property]);
415
415
  });
416
416
  },
417
- // Copmpare two strings in a case-insensitive way, returning -1, 0 or 1, suitable for use with sort().
417
+ // Compare two strings in a case-insensitive way, returning -1, 0 or 1, suitable for use with sort().
418
418
  // If the two strings represent numbers, compare them as numbers for a natural sort order
419
419
  // when comparing strings like '4' and '10'.
420
420
  insensitiveSortCompare(a, b) {
@@ -858,7 +858,7 @@ module.exports = {
858
858
  return _.startCase(o);
859
859
  },
860
860
 
861
- // check if something is a function (as opposd to property)
861
+ // check if something is a function (as opposed to property)
862
862
  isFunction: function(o) {
863
863
  return (typeof o === 'function');
864
864
  },
@@ -150,10 +150,12 @@ export default () => {
150
150
  if (options.busy) {
151
151
  if (!busyActive[busyName]) {
152
152
  busyActive[busyName] = 0;
153
- apos.bus.$emit('busy', {
154
- active: true,
155
- name: busyName
156
- });
153
+ if (apos.bus) {
154
+ apos.bus.$emit('busy', {
155
+ active: true,
156
+ name: busyName
157
+ });
158
+ }
157
159
  }
158
160
  // keep track of nested calls
159
161
  busyActive[busyName]++;
@@ -240,10 +242,12 @@ export default () => {
240
242
  busyActive[busyName]--;
241
243
  if (!busyActive[busyName]) {
242
244
  // if no nested calls, disable the "busy" state
243
- apos.bus.$emit('busy', {
244
- active: false,
245
- name: busyName
246
- });
245
+ if (apos.bus) {
246
+ apos.bus.$emit('busy', {
247
+ active: false,
248
+ name: busyName
249
+ });
250
+ }
247
251
  }
248
252
  }
249
253
  });
@@ -257,6 +257,21 @@ export default () => {
257
257
  return path + '.' + file.extension;
258
258
  };
259
259
 
260
+ // Given an asset path such as `/modules/modulename/images/file.png`, this
261
+ // method will return a URL for it. This is used when frontend JavaScript
262
+ // code needs to access static assets shipped in the `public` subdirectory of
263
+ // individual modules. Currently `path` must begin with `/modules/` followed
264
+ // by a module name; other namespaces may exist later. The remainder of the
265
+ // path, such as `/images/file.png` in the above example, must currespond
266
+ // to a file that exists in the `public` subdirectory of the named module.
267
+ //
268
+ // Asset paths of this type are also automatically supported by CSS and
269
+ // SCSS files in the project when using `url()`.
270
+
271
+ apos.util.assetUrl = function(path) {
272
+ return apos.assetBaseUrl + path;
273
+ };
274
+
260
275
  // Returns true if the uri references the same site (same host and port) as the
261
276
  // current page. Cross-browser implementation, valid at least back to IE11.
262
277
  // Regarding port numbers, this will match as long as the URIs are consistent
@@ -156,7 +156,7 @@ module.exports = {
156
156
  self.schema = self.apos.schema.compose({
157
157
  addFields: self.apos.schema.fieldsToArray(`Module ${self.__meta.name}`, self.fields),
158
158
  arrangeFields: self.apos.schema.groupsToArray(self.fieldsGroups)
159
- });
159
+ }, self);
160
160
  const forbiddenFields = [
161
161
  '_id',
162
162
  'type'
@@ -19,6 +19,7 @@
19
19
  :schema="schema"
20
20
  :value="docFields"
21
21
  @input="updateDocFields"
22
+ @validate="triggerValidate"
22
23
  :following-values="followingValues()"
23
24
  :conditional-fields="conditionalFields()"
24
25
  ref="schema"
@@ -23,7 +23,8 @@ export default {
23
23
  data() {
24
24
  return {
25
25
  rendered: '...',
26
- playerOpts: null
26
+ playerOpts: null,
27
+ playerEl: null
27
28
  };
28
29
  },
29
30
  mounted() {
@@ -40,6 +41,7 @@ export default {
40
41
  },
41
42
  methods: {
42
43
  async renderContent() {
44
+ const self = this;
43
45
  const parameters = {
44
46
  _docId: this.docId,
45
47
  widget: this.value,
@@ -61,6 +63,7 @@ export default {
61
63
  // AposAreas manager can spot any new area divs.
62
64
  // This will also run the player
63
65
  setTimeout(function() {
66
+ self.setPlayerEl();
64
67
  apos.bus.$emit('widget-rendered');
65
68
  }, 0);
66
69
  } catch (e) {
@@ -68,13 +71,18 @@ export default {
68
71
  console.error('Unable to render widget. Possibly the schema has been changed and the existing widget does not pass validation.', e);
69
72
  }
70
73
  },
71
- runPlayer() {
72
- if (!this.playerOpts) {
73
- return;
74
+ setPlayerEl() {
75
+ if (this.playerOpts) {
76
+ const el = this.$el.querySelector(this.playerOpts.selector);
77
+ if (el && this.playerOpts.player) {
78
+ this.playerEl = el;
79
+ }
74
80
  }
75
- const el = this.$el.querySelector(this.playerOpts.selector);
76
- if (el && this.playerOpts.player) {
77
- this.playerOpts.player(el);
81
+ },
82
+ runPlayer() {
83
+ if (this.playerEl && !this.playerEl.aposWidgetPlayed) {
84
+ this.playerOpts.player(this.playerEl);
85
+ this.playerEl.aposWidgetPlayed = true;
78
86
  }
79
87
  },
80
88
  clicked(e) {
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "apostrophe",
3
- "version": "3.4.1",
3
+ "version": "3.8.0",
4
4
  "description": "The Apostrophe Content Management System.",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
- "pretest": "npm run lint && npm audit",
7
+ "pretest": "npm run lint",
8
8
  "test": "nyc --reporter=html mocha -t 10000",
9
9
  "lint": "eslint . && node scripts/lint-i18n"
10
10
  },
@@ -112,7 +112,7 @@
112
112
  "vue": "^2.6.14",
113
113
  "vue-click-outside-element": "^1.0.13",
114
114
  "vue-loader": "^15.9.6",
115
- "vue-material-design-icons": "^4.9.0",
115
+ "vue-material-design-icons": "~4.12.1",
116
116
  "vue-style-loader": "^4.1.2",
117
117
  "vue-template-compiler": "^2.6.14",
118
118
  "vuedraggable": "^2.24.3",
@@ -127,7 +127,7 @@
127
127
  "eslint-loader": "^4.0.2",
128
128
  "eslint-plugin-node": "^11.1.0",
129
129
  "eslint-plugin-vue": "^7.9.0",
130
- "mocha": "^7.1.2",
130
+ "mocha": "^9.1.2",
131
131
  "nyc": "^15.1.0",
132
132
  "replace-in-file": "^6.1.0",
133
133
  "vue-eslint-parser": "^7.1.1",
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ improve: '@apostrophecms/global',
3
+ options: {
4
+ testGlobalLevelLoaded: true,
5
+ testGlobalLevel: true
6
+ }
7
+ };
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ improve: '@apostrophecms/piece-type',
3
+ options: {
4
+ testPieceTypeLevelLoaded: true,
5
+ testPieceTypeLevel: true
6
+ }
7
+ };
@@ -0,0 +1,30 @@
1
+ const t = require('../test-lib/test.js');
2
+ const assert = require('assert');
3
+
4
+ describe('Improve Overrides', function() {
5
+
6
+ this.timeout(t.timeout);
7
+
8
+ it('"improve" should work, but project level should override it', async function() {
9
+ let apos;
10
+ try {
11
+ apos = await t.create({
12
+ root: module,
13
+ modules: {
14
+ 'improve-piece-type': {},
15
+ 'improve-global': {}
16
+ }
17
+ });
18
+ assert(apos.global.options.verifyProjectLevelLoaded);
19
+ assert.strictEqual(apos.user.options.testPieceTypeLevelLoaded, true);
20
+ assert.strictEqual(apos.user.options.testPieceTypeLevel, true);
21
+ assert.strictEqual(apos.global.options.testPieceTypeLevelLoaded, true);
22
+ assert.strictEqual(apos.global.options.testPieceTypeLevel, false);
23
+ assert.strictEqual(apos.global.options.testGlobalLevelLoaded, true);
24
+ assert.strictEqual(apos.global.options.testGlobalLevel, false);
25
+ } finally {
26
+ t.destroy(apos);
27
+ }
28
+ });
29
+
30
+ });
package/test/job.js ADDED
@@ -0,0 +1,224 @@
1
+ const t = require('../test-lib/test.js');
2
+ const assert = require('assert');
3
+ const Promise = require('bluebird');
4
+ let apos;
5
+
6
+ describe('Job module', function() {
7
+
8
+ this.timeout(t.timeout);
9
+
10
+ after(async function() {
11
+ return t.destroy(apos);
12
+ });
13
+
14
+ let jobModule;
15
+
16
+ it('should be a property of the apos object', async function() {
17
+ this.timeout(t.timeout);
18
+ this.slow(2000);
19
+
20
+ apos = await t.create({
21
+ root: module,
22
+ modules: {
23
+ article: {
24
+ extend: '@apostrophecms/piece-type'
25
+ }
26
+ }
27
+ });
28
+ jobModule = apos.modules['@apostrophecms/job'];
29
+ assert(apos.modules['@apostrophecms/job']);
30
+ });
31
+
32
+ it('has a related database collection', async function () {
33
+ assert(jobModule.db);
34
+ });
35
+
36
+ let jobOne;
37
+
38
+ it('should create a new job', async function () {
39
+ jobOne = await jobModule.start({});
40
+
41
+ assert(jobOne._id);
42
+
43
+ const found = await jobModule.db.findOne({ _id: jobOne._id });
44
+
45
+ assert(found);
46
+ assert(found.status === 'running');
47
+ assert(found.ended === false);
48
+ });
49
+
50
+ it('should end a job and mark it as successful', async function () {
51
+ const result = await jobModule.end(jobOne, 'success', { testing: 'testing' });
52
+
53
+ assert(result.result.nModified === 1);
54
+
55
+ const found = await jobModule.db.findOne({ _id: jobOne._id });
56
+
57
+ assert(found);
58
+ assert(found.status === 'completed');
59
+ assert(found.ended === true);
60
+ });
61
+ let jar;
62
+ it('should get admin jar', async () => {
63
+ await t.createAdmin(apos);
64
+
65
+ jar = await t.getUserJar(apos);
66
+
67
+ assert(jar);
68
+ });
69
+
70
+ it('should access a job via REST API GET request', async function () {
71
+ const job = await apos.http.get(`/api/v1/@apostrophecms/job/${jobOne._id}`, {
72
+ jar
73
+ });
74
+
75
+ assert(job._id === jobOne._id);
76
+ });
77
+
78
+ let articleIds;
79
+
80
+ it('can insert many test articles', async function () {
81
+ const req = apos.task.getReq();
82
+
83
+ const promises = [];
84
+
85
+ for (let i = 1; i <= 500; i++) {
86
+ promises.push(insert(req, apos.modules.article, 'article', {}, i));
87
+ }
88
+
89
+ const inserted = await Promise.all(promises);
90
+ articleIds = inserted.map(doc => doc._id);
91
+
92
+ assert(inserted.length === 500);
93
+ assert(!!inserted[0]._id);
94
+ });
95
+
96
+ let jobTwo;
97
+ it('can run a batch job', async function () {
98
+ const req = apos.task.getReq();
99
+
100
+ jobTwo = await jobModule.runBatch(
101
+ req,
102
+ articleIds,
103
+ async function(req, id) {
104
+ await apos.doc.db.updateOne({
105
+ _id: id
106
+ }, {
107
+ $set: {
108
+ checked: true
109
+ }
110
+ });
111
+ }
112
+ );
113
+
114
+ assert(!!jobTwo.jobId);
115
+ });
116
+
117
+ it('can follow the second job as it works', async function () {
118
+ const { completed } = await pollJob({
119
+ route: `${jobModule.action}/${jobTwo.jobId}`
120
+ }, {
121
+ jar
122
+ });
123
+
124
+ assert(completed === articleIds.length);
125
+ const index = Math.floor(Math.random() * (articleIds.length - 1));
126
+
127
+ const article = await apos.http.get(`/api/v1/article/${articleIds[index]}`, {
128
+ jar
129
+ });
130
+
131
+ assert(article.checked === true);
132
+ });
133
+
134
+ const logged = [];
135
+
136
+ let jobThree;
137
+
138
+ it('can run a generic job', async function () {
139
+ const req = apos.task.getReq();
140
+
141
+ jobThree = await jobModule.run(
142
+ req,
143
+ async function(req, reporters) {
144
+ let count = 1;
145
+ reporters.setTotal(articleIds.length);
146
+
147
+ for (const id of articleIds) {
148
+ await Promise.delay(3);
149
+ logged.push(id);
150
+ if (count % 2) {
151
+ reporters.success();
152
+ } else {
153
+ reporters.failure();
154
+ }
155
+ count++;
156
+ }
157
+ }
158
+ );
159
+
160
+ assert(!!jobThree.jobId);
161
+ });
162
+
163
+ it('can follow the third job as it works', async function () {
164
+ const route = `${jobModule.action}/${jobThree.jobId}`;
165
+ const { total } = await apos.http.get(route, { jar });
166
+ // Tests setTotal()
167
+ assert(total === articleIds.length);
168
+
169
+ const {
170
+ completed,
171
+ good,
172
+ bad
173
+ } = await pollJob({
174
+ route
175
+ }, {
176
+ jar
177
+ });
178
+
179
+ assert(completed === articleIds.length);
180
+ // Tests success()
181
+ assert(good === (articleIds.length / 2));
182
+ // Tests failure()
183
+ assert(bad === (articleIds.length / 2));
184
+ });
185
+ });
186
+
187
+ function padInteger (i, places) {
188
+ let s = i + '';
189
+ while (s.length < places) {
190
+ s = '0' + s;
191
+ }
192
+ return s;
193
+ }
194
+
195
+ async function insert (req, pieceModule, title, data, i) {
196
+ const docData = Object.assign(pieceModule.newInstance(), {
197
+ title: `${title} #${padInteger(i, 5)}`,
198
+ slug: `${title}-${padInteger(i, 5)}`,
199
+ ...data
200
+ });
201
+
202
+ return pieceModule.insert(req, docData);
203
+ };
204
+
205
+ async function pollJob(job, { jar }) {
206
+ const {
207
+ processed,
208
+ total,
209
+ good,
210
+ bad
211
+ } = await apos.http.get(job.route, { jar });
212
+
213
+ if (processed < total) {
214
+ Promise.delay(100);
215
+
216
+ return await pollJob(job, { jar });
217
+ } else {
218
+ return {
219
+ completed: processed,
220
+ good,
221
+ bad
222
+ };
223
+ }
224
+ }