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.
- package/.eslintrc +4 -0
- package/.scratch.md +2 -0
- package/CHANGELOG.md +114 -2
- package/README.md +1 -1
- package/deploy-test-count +1 -1
- package/index.js +125 -5
- package/lib/moog-require.js +41 -3
- package/lib/moog.js +20 -8
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarLocale.vue +42 -23
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +30 -14
- package/modules/@apostrophecms/area/index.js +9 -0
- package/modules/@apostrophecms/area/lib/custom-tags/area.js +1 -1
- package/modules/@apostrophecms/area/lib/custom-tags/widget.js +1 -1
- package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +3 -0
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +6 -6
- package/modules/@apostrophecms/asset/index.js +85 -21
- package/modules/@apostrophecms/asset/lib/globalIcons.js +2 -0
- package/modules/@apostrophecms/asset/lib/webpack/src/webpack.scss.js +5 -2
- package/modules/@apostrophecms/attachment/index.js +1 -0
- package/modules/@apostrophecms/db/index.js +5 -6
- package/modules/@apostrophecms/doc/index.js +13 -3
- package/modules/@apostrophecms/doc-type/index.js +24 -4
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +13 -1
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +3 -0
- package/modules/@apostrophecms/i18n/i18n/en.json +26 -6
- package/modules/@apostrophecms/i18n/i18n/es.json +382 -0
- package/modules/@apostrophecms/i18n/i18n/pt-BR.json +379 -0
- package/modules/@apostrophecms/i18n/i18n/sk.json +380 -0
- package/modules/@apostrophecms/i18n/index.js +10 -1
- package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalize.vue +153 -121
- package/modules/@apostrophecms/image/index.js +2 -1
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +6 -3
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerEditor.vue +24 -13
- package/modules/@apostrophecms/image-widget/index.js +2 -1
- package/modules/@apostrophecms/image-widget/views/widget.html +12 -2
- package/modules/@apostrophecms/job/index.js +164 -212
- package/modules/@apostrophecms/login/index.js +36 -17
- package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +8 -0
- package/modules/@apostrophecms/migration/index.js +1 -1
- package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +151 -61
- package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +6 -2
- package/modules/@apostrophecms/modal/ui/apos/components/AposModalConfirm.vue +9 -7
- package/modules/@apostrophecms/modal/ui/apos/mixins/AposDocsManagerMixin.js +12 -15
- package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +6 -0
- package/modules/@apostrophecms/module/index.js +1 -1
- package/modules/@apostrophecms/notification/index.js +116 -8
- package/modules/@apostrophecms/notification/ui/apos/components/AposNotification.vue +89 -11
- package/modules/@apostrophecms/notification/ui/apos/components/TheAposNotifications.vue +1 -1
- package/modules/@apostrophecms/page/index.js +37 -30
- package/modules/@apostrophecms/permission/index.js +1 -1
- package/modules/@apostrophecms/permission/ui/apos/components/AposInputRole.vue +4 -2
- package/modules/@apostrophecms/piece-type/index.js +178 -61
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +179 -47
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerDisplay.vue +1 -3
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerSelectBox.vue +138 -0
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +42 -10
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapStyles.vue +3 -0
- package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Classes.js +6 -10
- package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Default.js +64 -0
- package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Document.js +15 -0
- package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Heading.js +23 -0
- package/modules/@apostrophecms/schema/index.js +97 -20
- package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +1 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +4 -1
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputAttachment.vue +11 -160
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputRadio.vue +8 -5
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +24 -2
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +24 -6
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +0 -4
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +0 -7
- package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +25 -3
- package/modules/@apostrophecms/schema/ui/apos/mixins/AposInputMixin.js +10 -2
- package/modules/@apostrophecms/task/index.js +2 -2
- package/modules/@apostrophecms/template/index.js +63 -36
- package/modules/@apostrophecms/template/lib/custom-tags/component.js +1 -1
- package/modules/@apostrophecms/template/lib/custom-tags/render.js +6 -2
- package/modules/@apostrophecms/ui/index.js +6 -2
- package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +21 -3
- package/modules/@apostrophecms/ui/ui/apos/components/AposCellContextMenu.vue +1 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposFile.vue +205 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposIndicator.vue +5 -0
- package/modules/@apostrophecms/ui/ui/apos/lib/i18next.js +16 -2
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_tables.scss +4 -3
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_theme.scss +3 -0
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_widgets.scss +3 -0
- package/modules/@apostrophecms/ui/ui/apos/scss/global/import-all.scss +2 -1
- package/modules/@apostrophecms/user/index.js +21 -0
- package/modules/@apostrophecms/util/index.js +2 -2
- package/modules/@apostrophecms/util/ui/src/http.js +12 -8
- package/modules/@apostrophecms/util/ui/src/util.js +15 -0
- package/modules/@apostrophecms/widget-type/index.js +1 -1
- package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +1 -0
- package/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js +15 -7
- package/package.json +4 -4
- package/test/extra_node_modules/improve-global/index.js +7 -0
- package/test/extra_node_modules/improve-piece-type/index.js +7 -0
- package/test/improve-overrides.js +30 -0
- package/test/job.js +224 -0
- package/test/login.js +183 -0
- package/test/modules/@apostrophecms/global/index.js +8 -0
- package/test/modules/fragment-all/views/aux-test.html +7 -0
- package/test/modules/fragment-all/views/fragment.html +5 -0
- package/test/moog.js +47 -0
- package/test/package.json +5 -4
- package/test/pieces.js +17 -0
- package/test/reverse-relationship.js +170 -0
- package/test/subdir-project/app.js +3 -0
- package/test/subdir-project.js +26 -0
- package/test/templates.js +7 -1
- package/test-lib/test.js +23 -12
- 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
|
|
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
|
|
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
|
-
|
|
60
|
+
padding-right: 0;
|
|
61
|
+
padding-left: 0;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
.apos-table__cell-field--context-menu {
|
|
@@ -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
|
-
//
|
|
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
|
|
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
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
244
|
-
|
|
245
|
-
|
|
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'
|
|
@@ -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
|
-
|
|
72
|
-
if (
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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.
|
|
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
|
|
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": "
|
|
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": "^
|
|
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,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
|
+
}
|