apostrophe 3.59.0 → 3.60.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/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.60.0 (2023-11-29)
4
+
5
+ ### Adds
6
+
7
+ * Add the possibility to add custom classes to notifications.
8
+ Setting the `apos-notification--hidden` class will hide the notification, which can be useful when we only care about the event carried by it.
9
+ * Give the possibility to add horizontal rules from the insert menu of the rich text editor with the following widget option: `insert: [ 'horizontalRule' ]`.
10
+ Improve also the UX to focus back the editor after inserting a horizontal rule or a table.
11
+
12
+ ### Fixes
13
+
14
+ * The `render-widget` route now provides an `options` property on the widget, so that
15
+ schema-level options of the widget are available to the external front end when
16
+ rendering a newly added or edited widget in the editor. Note that when rendering a full page,
17
+ this information is already available on the parent area: `area.options.widgets[widget.type]`
18
+ * Pages inserted directly in the published mode are now given a
19
+ correct `lastPublishedAt` property, correcting several bugs relating
20
+ to the page tree.
21
+ * A migration has been added to introduce `lastPublishedAt` wherever
22
+ it is missing for existing pages.
23
+ * Fixed a bug that prevented page ranks from renumbering properly during "insert after" operations.
24
+ * Added a one-time migration to make existing page ranks unique among peers.
25
+ * Fixes conditional fields not being properly updated when switching items in array editor.
26
+ * The `beforeSend` event for pages and the loading of deferred widgets are now
27
+ handled in `renderPage` with the proper timing so that areas can be annotated
28
+ successfully for "external front" use.
29
+ * The external front now receives 100% of the serialization-friendly data that Nunjucks receives,
30
+ including the `home` property etc. Note that the responsibility to avoid passing any nonserializable
31
+ or excessively large data in `req.data` falls on the developer when choosing to use the
32
+ `apos-external-front` feature.
33
+ * Wraps the group label in the expanded preview menu component in `$t()` to allow translation
34
+
35
+ ## 3.59.1 (2023-11-14)
36
+
37
+ ### Fixes
38
+
39
+ * Fix `if` and `requiredIf` fields inside arrays. With regard to `if`, this is a hotfix for a regression introduced in 3.59.0.
40
+
3
41
  ## 3.59.0 (2023-11-03)
4
42
 
5
43
  ### Changes
@@ -71,6 +71,7 @@ module.exports = {
71
71
  if (req.aposExternalFront) {
72
72
  const result = {
73
73
  ...req.data,
74
+ options,
74
75
  widget
75
76
  };
76
77
  return result;
@@ -16,7 +16,7 @@
16
16
  :key="groupIndex"
17
17
  class="apos-widget-group"
18
18
  >
19
- <h2 class="apos-widget-group__label" v-if="group.label">{{ group.label }}</h2>
19
+ <h2 class="apos-widget-group__label" v-if="group.label">{{ $t(group.label) }}</h2>
20
20
  <div
21
21
  :class="[
22
22
  `apos-widget-group--${group.columns}-column${
@@ -239,13 +239,22 @@ module.exports = {
239
239
  })) {
240
240
  return;
241
241
  }
242
+ const lastPublishedAt = doc.createdAt || new Date();
242
243
  const draft = {
243
244
  ...doc,
244
245
  _id: draftId,
245
246
  aposLocale: draftLocale,
246
- lastPublishedAt: doc.createdAt || new Date()
247
+ lastPublishedAt
247
248
  };
248
- return manager.insertDraftOf(req, doc, draft, options);
249
+ await manager.insertDraftOf(req, doc, draft, options);
250
+ // Published doc must know it is published, otherwise various bugs ensue
251
+ return self.apos.doc.db.updateOne({
252
+ _id: doc._id
253
+ }, {
254
+ $set: {
255
+ lastPublishedAt
256
+ }
257
+ });
249
258
  }
250
259
  },
251
260
  fixUniqueError: {
@@ -285,6 +285,7 @@ module.exports = {
285
285
  ids: options.ids
286
286
  },
287
287
  event,
288
+ classes: options.classes,
288
289
  icon: req.body.messages.icon || 'database-export-icon',
289
290
  type: options.type || 'success',
290
291
  return: true
@@ -379,8 +379,8 @@ module.exports = {
379
379
  return self.apos.template.renderStringForModule(req, s, data, self);
380
380
  },
381
381
 
382
- // TIP: you probably want `self.sendPage`, which loads
383
- // `data.home` for you and also sends the response to the browser.
382
+ // TIP: more often you will want `self.sendPage`, which also sends the response
383
+ // to the browser.
384
384
  //
385
385
  // This method generates a complete HTML page for transmission to the
386
386
  // browser. Returns HTML markup ready to send (but `self.sendPage` is
@@ -419,11 +419,19 @@ module.exports = {
419
419
  //
420
420
  // This method is async in 3.x and must be awaited.
421
421
  //
422
+ // If the external front feature is in use for the request, then
423
+ // self.apos.template.annotateDataForExternalFront and
424
+ // self.apos.template.pruneDataForExternalFront are called
425
+ // and the data is returned, in place of normal Nunjucks rendering.
426
+ //
422
427
  // No longer deprecated because it is a useful override point
423
428
  // for this part of the behavior of sendPage.
424
429
 
425
430
  async renderPage(req, template, data) {
431
+ await self.apos.page.emit('beforeSend', req);
432
+ await self.apos.area.loadDeferredWidgets(req);
426
433
  if (req.aposExternalFront) {
434
+ data = self.apos.template.getRenderDataArgs(req, data, self);
427
435
  await self.apos.template.annotateDataForExternalFront(req, template, data);
428
436
  self.apos.template.pruneDataForExternalFront(req, template, data);
429
437
  // Reply with JSON
@@ -484,8 +492,6 @@ module.exports = {
484
492
  span.setAttribute(telemetry.Attributes.TEMPLATE, template);
485
493
 
486
494
  try {
487
- await self.apos.page.emit('beforeSend', req);
488
- await self.apos.area.loadDeferredWidgets(req);
489
495
  const result = await self.renderPage(req, template, data);
490
496
  req.res.send(result);
491
497
  span.setStatus({ code: telemetry.api.SpanStatusCode.OK });
@@ -99,6 +99,7 @@ module.exports = {
99
99
  ], 'info');
100
100
  const icon = self.apos.launder.string(req.body.icon);
101
101
  const message = self.apos.launder.string(req.body.message);
102
+ const classes = self.apos.launder.strings(req.body.classes);
102
103
  const interpolate = launderInterpolate(req.body.interpolate);
103
104
  const dismiss = self.apos.launder.integer(req.body.dismiss);
104
105
  let buttons = req.body.buttons;
@@ -118,6 +119,7 @@ module.exports = {
118
119
  }));
119
120
  }
120
121
  return self.trigger(req, message, {
122
+ classes,
121
123
  interpolate,
122
124
  dismiss,
123
125
  icon,
@@ -280,7 +282,8 @@ module.exports = {
280
282
  localize: has(req.body, 'localize')
281
283
  ? self.apos.launder.boolean(req.body.localize) : true,
282
284
  job: options.job || null,
283
- event: options.event
285
+ event: options.event,
286
+ classes: options.classes || null
284
287
  };
285
288
 
286
289
  if (copiedOptions.dismiss === true) {
@@ -79,6 +79,11 @@ export default {
79
79
  computed: {
80
80
  classList() {
81
81
  const classes = [ 'apos-notification' ];
82
+
83
+ if (Array.isArray(this.notification.classes) && this.notification.classes.length) {
84
+ classes.push(...this.notification.classes);
85
+ }
86
+
82
87
  if (this.notification.type && this.notification.type !== 'none') {
83
88
  classes.push(`apos-notification--${this.notification.type}`);
84
89
  }
@@ -223,6 +228,10 @@ export default {
223
228
  }
224
229
  }
225
230
 
231
+ .apos-notification--hidden {
232
+ display: none;
233
+ }
234
+
226
235
  .apos-notification--long {
227
236
  border-radius: 10px;
228
237
  }
@@ -101,6 +101,8 @@ module.exports = {
101
101
  self.addLegacyMigrations();
102
102
  self.addMisreplicatedParkedPagesMigration();
103
103
  self.addDuplicateParkedPagesMigration();
104
+ self.apos.migration.add('deduplicateRanks2', self.deduplicateRanks2Migration);
105
+ self.apos.migration.add('missingLastPublishedAt', self.missingLastPublishedAtMigration);
104
106
  await self.createIndexes();
105
107
  },
106
108
  restApiRoutes(self) {
@@ -850,8 +852,8 @@ database.`);
850
852
  const query = self.find(req, criteria, options).permission('edit').archived(null);
851
853
  return query;
852
854
  },
853
- // Insert a page. `targetId` must be an existing page id, and
854
- // `position` may be `before`, `inside` or `after`. Alternatively
855
+ // Insert a page. `targetId` must be an existing page id, `_archive` or
856
+ // `_home`, and `position` may be `before`, `inside` or `after`. Alternatively
855
857
  // `position` may be a zero-based offset for the new child
856
858
  // of `targetId` (note that the `rank` property of sibling pages
857
859
  // is not strictly ascending, so use an array index into `_children` to
@@ -930,7 +932,7 @@ database.`);
930
932
  return self.insert(req, target._id, 'before', page, options);
931
933
  }
932
934
  page.rank = target.rank + 1;
933
- const index = peers.findIndex(peer => peer.id === target._id);
935
+ const index = peers.findIndex(peer => peer._id === target._id);
934
936
  if (index !== -1) {
935
937
  pushed = peers.slice(index + 1).map(peer => peer._id);
936
938
  }
@@ -2503,6 +2505,84 @@ database.`);
2503
2505
  }
2504
2506
  });
2505
2507
  },
2508
+ async deduplicateRanks2Migration() {
2509
+ for (const locale of Object.keys(self.apos.i18n.locales)) {
2510
+ for (const mode of [ 'previous', 'draft', 'published' ]) {
2511
+ const pages = await self.apos.doc.db.find({
2512
+ slug: /^\//,
2513
+ aposLocale: `${locale}:${mode}`
2514
+ }, {
2515
+ path: 1,
2516
+ rank: 1,
2517
+ slug: 1
2518
+ }).toArray();
2519
+ const pagesByPath = new Map();
2520
+ for (const page of pages) {
2521
+ page._children = [];
2522
+ pagesByPath.set(page.path, page);
2523
+ }
2524
+ for (const page of pages) {
2525
+ if (page.level === 0) {
2526
+ // Home page has no parent
2527
+ continue;
2528
+ }
2529
+ const parentPath = self.getParentPath(page);
2530
+ const parent = pagesByPath.get(parentPath);
2531
+ if (!parent) {
2532
+ self.apos.util.error(`Warning: page ${page._id} has no parent in the tree`);
2533
+ continue;
2534
+ }
2535
+ parent._children.push(page);
2536
+ }
2537
+ for (const page of pages) {
2538
+ const children = page._children;
2539
+ children.sort((a, b) => a.rank - b.rank);
2540
+ let lastRank = null;
2541
+ let bad = false;
2542
+ for (const child of children) {
2543
+ if (child.rank === lastRank) {
2544
+ bad = true;
2545
+ break;
2546
+ }
2547
+ lastRank = child.rank;
2548
+ }
2549
+ if (bad) {
2550
+ self.apos.util.warn(`Fixing ranks for children of ${page.slug} in ${page.aposLocale}`);
2551
+ for (let i = 0; (i < children.length); i++) {
2552
+ await self.apos.doc.db.updateOne({
2553
+ _id: children[i]._id
2554
+ }, {
2555
+ $set: {
2556
+ rank: i
2557
+ }
2558
+ });
2559
+ }
2560
+ }
2561
+ }
2562
+ }
2563
+ }
2564
+ },
2565
+ missingLastPublishedAtMigration() {
2566
+ return self.apos.migration.eachDoc({
2567
+ aposMode: 'published',
2568
+ lastPublishedAt: null
2569
+ }, async doc => {
2570
+ const draft = await self.apos.doc.db.findOne({
2571
+ _id: doc._id.replace(':published', ':draft')
2572
+ });
2573
+ if (!draft) {
2574
+ self.apos.util.error(`Warning: published document has no matching draft: ${doc._id}`);
2575
+ return;
2576
+ }
2577
+ await self.apos.doc.db.updateOne({
2578
+ _id: doc._id
2579
+ }, {
2580
+ $set: {
2581
+ lastPublishedAt: draft.lastPublishedAt
2582
+ }
2583
+ });
2584
+ });
2585
+ },
2506
2586
  async inferLastTargetIdAndPosition(doc) {
2507
2587
  const parentPath = self.getParentPath(doc);
2508
2588
  const parentAposDocId = parentPath.split('/').pop();
@@ -220,6 +220,11 @@ module.exports = {
220
220
  label: 'apostrophe:image',
221
221
  description: 'apostrophe:imageDescription',
222
222
  component: 'AposImageControlDialog'
223
+ },
224
+ horizontalRule: {
225
+ icon: 'minus-icon',
226
+ label: 'apostrophe:richTextHorizontalRule',
227
+ action: 'setHorizontalRule'
223
228
  }
224
229
  },
225
230
  // Additional properties used in executing tiptap commands
@@ -590,6 +590,7 @@ export default {
590
590
  } else {
591
591
  this.removeSlash();
592
592
  this.editor.commands[info.action || name]();
593
+ this.editor.commands.focus();
593
594
  }
594
595
  },
595
596
  removeSlash() {
@@ -498,10 +498,10 @@ module.exports = {
498
498
  }
499
499
  continue;
500
500
  } else if (val.$ne) {
501
- // eslint-disable-next-line eqeqeq
502
- if (val.$ne == destinationKey) {
501
+ if (val.$ne === destinationKey) {
503
502
  return false;
504
503
  }
504
+ continue;
505
505
  }
506
506
 
507
507
  // Handle external conditions:
@@ -526,23 +526,22 @@ module.exports = {
526
526
  continue;
527
527
  }
528
528
 
529
- if (val.min && destinationKey < val.min) {
530
- return false;
531
- }
532
- if (val.max && destinationKey > val.max) {
533
- return false;
529
+ // test with Object.prototype for the case val.min === 0
530
+ if (Object.hasOwn(val, 'min') || Object.hasOwn(val, 'max')) {
531
+ if (destinationKey < val.min) {
532
+ return false;
533
+ }
534
+ if (destinationKey > val.max) {
535
+ return false;
536
+ }
537
+ continue;
534
538
  }
535
539
 
536
540
  if (conditionalFields?.[key] === false) {
537
541
  return false;
538
542
  }
539
543
 
540
- if (typeof val === 'boolean' && !destinationKey) {
541
- return false;
542
- }
543
-
544
- // eslint-disable-next-line eqeqeq
545
- if ((typeof val === 'string' || typeof val === 'number') && destinationKey != val) {
544
+ if (destinationKey !== val) {
546
545
  return false;
547
546
  }
548
547
  }
@@ -138,9 +138,8 @@ export default {
138
138
  async mounted() {
139
139
  this.modal.active = true;
140
140
  await this.evaluateExternalConditions();
141
- this.evaluateConditions();
142
141
  if (this.next.length) {
143
- this.select(this.next[0]._id);
142
+ await this.select(this.next[0]._id);
144
143
  }
145
144
  if (this.serverError && this.serverError.data && this.serverError.data.errors) {
146
145
  const first = this.serverError.data.errors[0];
@@ -168,6 +167,7 @@ export default {
168
167
  hasErrors: false,
169
168
  data: this.next.find(item => item._id === _id)
170
169
  };
170
+ this.evaluateConditions();
171
171
  this.triggerValidation = false;
172
172
  }
173
173
  },
@@ -193,7 +193,7 @@ export default {
193
193
  const item = this.newInstance();
194
194
  item._id = cuid();
195
195
  this.next.push(item);
196
- this.select(item._id);
196
+ await this.select(item._id);
197
197
  this.updateMinMax();
198
198
  }
199
199
  },
@@ -276,35 +276,15 @@ module.exports = {
276
276
  },
277
277
 
278
278
  // Implementation detail of `renderBody` responsible for
279
- // creating the input object passed to Nunjucks for rendering,
280
- // with `data` merged into the `.data` property,
281
- // `apos` available separately, `__req` available separately, etc.
279
+ // creating the input object passed to the template engine e.g. Nunjucks.
280
+ // Includes both serializable data like `user` and non-JSON-friendly
281
+ // properties like `apos`, `getOptions()` and `__req`. If you are only
282
+ // interested in serializable data use `getRenderDataArgs`
282
283
 
283
284
  getRenderArgs(req, data, module) {
284
- const merged = {};
285
-
286
- if (data) {
287
- _.defaults(merged, data);
288
- }
289
-
290
- const args = {};
291
-
292
- args.data = merged;
293
-
294
- if (req.data) {
295
- _.defaults(merged, req.data);
296
- }
297
- _.defaults(merged, {
298
- user: req.user,
299
- permissions: (req.user && req.user._permissions) || {}
300
- });
301
-
302
- if (module.templateData) {
303
- _.defaults(merged, module.templateData);
304
- }
305
-
306
- args.data.locale = args.data.locale || req.locale;
307
-
285
+ const args = {
286
+ data: self.getRenderDataArgs(req, data, module)
287
+ };
308
288
  args.apos = self.templateApos;
309
289
  args.__t = req.t;
310
290
  args.__ = key => {
@@ -328,6 +308,33 @@ module.exports = {
328
308
  return args;
329
309
  },
330
310
 
311
+ // Just the external front-compatible parts of `getRenderArgs` that
312
+ // go into `args.data` for Nunjucks, e.g. merging `req.data` and `data`, adding
313
+ // `req.user` as `user`, etc.
314
+
315
+ getRenderDataArgs(req, data, module) {
316
+ const merged = {};
317
+
318
+ if (data) {
319
+ _.defaults(merged, data);
320
+ }
321
+
322
+ if (req.data) {
323
+ _.defaults(merged, req.data);
324
+ }
325
+ _.defaults(merged, {
326
+ user: req.user,
327
+ permissions: (req.user && req.user._permissions) || {}
328
+ });
329
+
330
+ if (module.templateData) {
331
+ _.defaults(merged, module.templateData);
332
+ }
333
+
334
+ merged.locale = merged.locale || req.locale;
335
+ return merged;
336
+ },
337
+
331
338
  // Fetch a nunjucks environment in which `include`, `extends`, etc. search
332
339
  // the views directories of the specified module and its ancestors.
333
340
  // Typically you will call `self.render` on your module
@@ -897,7 +904,7 @@ module.exports = {
897
904
  },
898
905
 
899
906
  getDocsForExternalFront(req, template, data) {
900
- return [ data.page, data.piece, ...(data.pieces || []) ].filter(doc => !!doc);
907
+ return [ data.home, ...(data.page?._ancestors || []), ...(data.page?._children || []), data.page, data.piece, ...(data.pieces || []) ].filter(doc => !!doc);
901
908
  },
902
909
 
903
910
  annotateDocForExternalFront(doc) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apostrophe",
3
- "version": "3.59.0",
3
+ "version": "3.60.0",
4
4
  "description": "The Apostrophe Content Management System.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,43 @@
1
+ const t = require('../test-lib/test.js');
2
+ const assert = require('assert');
3
+
4
+ let apos;
5
+ // Set env var so these tests work even if you have a dev key in your bashrc etc.
6
+ process.env.APOS_EXTERNAL_FRONT_KEY = 'this is a test external front key';
7
+
8
+ describe('External Front', function() {
9
+
10
+ this.timeout(t.timeout);
11
+
12
+ after(function() {
13
+ return t.destroy(apos);
14
+ });
15
+
16
+ it('apostrophe should initialize normally', async function() {
17
+ apos = await t.create({
18
+ root: module
19
+ });
20
+
21
+ assert(apos.page.__meta.name === '@apostrophecms/page');
22
+ });
23
+
24
+ it('fetch home with external front', async function() {
25
+ const data = await await apos.http.get('/', {
26
+ headers: {
27
+ 'x-requested-with': 'AposExternalFront',
28
+ 'apos-external-front-key': process.env.APOS_EXTERNAL_FRONT_KEY
29
+ }
30
+ });
31
+ assert.strictEqual(typeof data, 'object');
32
+ assert(data.page);
33
+ assert(data.home);
34
+ assert(data.page.slug === data.home.slug);
35
+ assert(data.page.slug === '/');
36
+ });
37
+
38
+ it('fetch home normally', async function() {
39
+ const data = await await apos.http.get('/', {});
40
+ assert.strictEqual(typeof data, 'string');
41
+ assert(data.includes('Home Page Template'));
42
+ });
43
+ });
@@ -192,6 +192,7 @@ describe('Pages REST', function() {
192
192
  });
193
193
 
194
194
  it('should be able to use db to insert documents', async function() {
195
+ const lastPublishedAt = new Date();
195
196
  const testItems = [
196
197
  {
197
198
  _id: 'parent:en:published',
@@ -202,7 +203,8 @@ describe('Pages REST', function() {
202
203
  visibility: 'public',
203
204
  path: `${homeId.replace(':en:published', '')}/parent`,
204
205
  level: 1,
205
- rank: 0
206
+ rank: 0,
207
+ lastPublishedAt
206
208
  },
207
209
  {
208
210
  _id: 'child:en:published',
@@ -213,7 +215,8 @@ describe('Pages REST', function() {
213
215
  visibility: 'public',
214
216
  path: `${homeId.replace(':en:published', '')}/parent/child`,
215
217
  level: 2,
216
- rank: 0
218
+ rank: 0,
219
+ lastPublishedAt
217
220
  },
218
221
  {
219
222
  _id: 'grandchild:en:published',
@@ -224,7 +227,8 @@ describe('Pages REST', function() {
224
227
  visibility: 'public',
225
228
  path: `${homeId.replace(':en:published', '')}/parent/child/grandchild`,
226
229
  level: 3,
227
- rank: 0
230
+ rank: 0,
231
+ lastPublishedAt
228
232
  },
229
233
  {
230
234
  _id: 'sibling:en:published',
@@ -235,8 +239,8 @@ describe('Pages REST', function() {
235
239
  visibility: 'public',
236
240
  path: `${homeId.replace(':en:published', '')}/parent/sibling`,
237
241
  level: 2,
238
- rank: 1
239
-
242
+ rank: 1,
243
+ lastPublishedAt
240
244
  },
241
245
  {
242
246
  _id: 'cousin:en:published',
@@ -247,7 +251,8 @@ describe('Pages REST', function() {
247
251
  visibility: 'public',
248
252
  path: `${homeId.replace(':en:published', '')}/parent/sibling/cousin`,
249
253
  level: 3,
250
- rank: 0
254
+ rank: 0,
255
+ lastPublishedAt
251
256
  },
252
257
  {
253
258
  _id: 'another-parent:en:published',
@@ -258,7 +263,8 @@ describe('Pages REST', function() {
258
263
  visibility: 'public',
259
264
  path: `${homeId.replace(':en:published', '')}/another-parent`,
260
265
  level: 1,
261
- rank: 1
266
+ rank: 1,
267
+ lastPublishedAt
262
268
  },
263
269
  {
264
270
  _id: 'neighbor:en:published',
@@ -269,7 +275,8 @@ describe('Pages REST', function() {
269
275
  visibility: 'public',
270
276
  path: `${homeId.replace(':en:published', '')}/neighbor`,
271
277
  level: 1,
272
- rank: 2
278
+ rank: 2,
279
+ lastPublishedAt
273
280
  }
274
281
  ];
275
282
 
@@ -482,7 +489,6 @@ describe('Pages REST', function() {
482
489
  },
483
490
  jar
484
491
  });
485
-
486
492
  const cousin = await apos.http.get('/api/v1/@apostrophecms/page/cousin:en:published', { jar });
487
493
  const sibling = await apos.http.get('/api/v1/@apostrophecms/page/sibling:en:published', { jar });
488
494
 
@@ -0,0 +1,144 @@
1
+ const t = require('../test-lib/test.js');
2
+ const assert = require('assert');
3
+
4
+ let apos;
5
+
6
+ describe('Pages', function() {
7
+
8
+ this.timeout(t.timeout);
9
+
10
+ after(function() {
11
+ return t.destroy(apos);
12
+ });
13
+
14
+ // EXISTENCE
15
+
16
+ it('should be a property of the apos object', async function() {
17
+ apos = await t.create({
18
+ root: module,
19
+ modules: {
20
+ '@apostrophecms/page': {
21
+ options: {
22
+ park: [],
23
+ types: [
24
+ {
25
+ name: '@apostrophecms/home-page',
26
+ label: 'Home'
27
+ },
28
+ {
29
+ name: 'test-page',
30
+ label: 'Test Page'
31
+ }
32
+ ],
33
+ publicApiProjection: {
34
+ title: 1,
35
+ _url: 1
36
+ }
37
+ }
38
+ },
39
+ 'test-page': {
40
+ extend: '@apostrophecms/page-type'
41
+ }
42
+ }
43
+ });
44
+
45
+ assert(apos.page.__meta.name === '@apostrophecms/page');
46
+ });
47
+
48
+ it('inserting child pages with a published req should produce the correct draft/published pairs', async function() {
49
+ const req = apos.task.getAdminReq();
50
+ const manager = apos.doc.getManager('test-page');
51
+
52
+ for (let i = 1; (i <= 10); i++) {
53
+ const page = manager.newInstance();
54
+ page.title = `test-child-${i}`;
55
+ page.type = 'test-page';
56
+ const { _id } = await apos.page.insert(req, '_home', 'lastChild', page, {});
57
+ const fetchedPage = await apos.page.find(req, { _id }).toObject();
58
+ assert.strictEqual(fetchedPage.aposMode, 'published');
59
+ assert(fetchedPage);
60
+ const draftReq = req.clone({
61
+ mode: 'draft'
62
+ });
63
+ const draft = await apos.page.find(draftReq, {
64
+ aposDocId: fetchedPage.aposDocId
65
+ }).toObject();
66
+ assert(draft);
67
+ assert.strictEqual(draft.aposMode, 'draft');
68
+ assert(draft.level === fetchedPage.level);
69
+ assert(draft.lastPublishedAt);
70
+ assert(fetchedPage.lastPublishedAt);
71
+ assert(draft.lastPublishedAt.getTime() === fetchedPage.lastPublishedAt.getTime());
72
+ }
73
+ assert(checkRanks('en:published'));
74
+ assert(checkRanks('en:draft'));
75
+ });
76
+
77
+ it('Can fix the ranks after intentionally messing them up', async function() {
78
+ for (let i = 5; (i <= 10); i++) {
79
+ await apos.doc.db.updateMany({
80
+ title: `test-child-${i}`
81
+ }, {
82
+ $set: {
83
+ rank: i - 2
84
+ }
85
+ });
86
+ }
87
+ try {
88
+ await checkRanks('en:published');
89
+ assert(false);
90
+ } catch (e) {
91
+ // Good, supposed to fail
92
+ }
93
+ try {
94
+ await checkRanks('en:draft');
95
+ assert(false);
96
+ } catch (e) {
97
+ // Good, supposed to fail
98
+ }
99
+ await apos.page.deduplicateRanks2Migration();
100
+ await checkRanks('en:published');
101
+ await checkRanks('en:draft');
102
+ });
103
+
104
+ it('Can fix lastPublishedAt after intentionally messing it up', async function() {
105
+ let published = await apos.doc.db.findOne({
106
+ aposLocale: 'en:published',
107
+ slug: '/test-child-1'
108
+ });
109
+ assert(published.lastPublishedAt);
110
+ await apos.doc.db.updateOne({
111
+ _id: published._id
112
+ }, {
113
+ $unset: {
114
+ lastPublishedAt: 1
115
+ }
116
+ });
117
+ published = await apos.doc.db.findOne({
118
+ aposLocale: 'en:published',
119
+ slug: '/test-child-1'
120
+ });
121
+ assert(!published.lastPublishedAt);
122
+ await apos.page.missingLastPublishedAtMigration();
123
+ published = await apos.doc.db.findOne({
124
+ aposLocale: 'en:published',
125
+ slug: '/test-child-1'
126
+ });
127
+ assert(published.lastPublishedAt);
128
+ });
129
+ });
130
+
131
+ async function checkRanks(aposLocale) {
132
+ const pages = await apos.doc.db.find({
133
+ level: 1,
134
+ aposLocale
135
+ }).project({
136
+ slug: 1,
137
+ rank: 1,
138
+ title: 1
139
+ }).toArray();
140
+ for (let i = 1; (i <= 10); i++) {
141
+ assert(pages.find(page => (page.rank === i - 1) && page.title === `test-child-${i}`));
142
+ }
143
+ assert(pages.find(page => (page.slug === '/archive') && (page.rank === 10)));
144
+ }
package/test/schemas.js CHANGED
@@ -2381,7 +2381,9 @@ describe('Schemas', function() {
2381
2381
 
2382
2382
  assert.deepEqual(actual, expected);
2383
2383
  });
2384
+ });
2384
2385
 
2386
+ describe('field if|ifRequired', function () {
2385
2387
  it('should enforce required property not equal match', async function() {
2386
2388
  const req = apos.task.getReq();
2387
2389
  const schema = apos.schema.compose({
@@ -2468,10 +2470,7 @@ describe('Schemas', function() {
2468
2470
  subfield: false
2469
2471
  }
2470
2472
  }, output);
2471
- console.log('output', require('util').inspect(output, {
2472
- colors: true,
2473
- depth: 1
2474
- }));
2473
+
2475
2474
  assert(!output.requiredProp);
2476
2475
  });
2477
2476
 
@@ -2676,6 +2675,120 @@ describe('Schemas', function() {
2676
2675
  }, 'requiredProp', 'required');
2677
2676
  });
2678
2677
 
2678
+ it('should enforce required property number min', async function() {
2679
+ const req = apos.task.getReq();
2680
+ const schema = apos.schema.compose({
2681
+ addFields: [
2682
+ {
2683
+ name: 'prop1',
2684
+ type: 'integer',
2685
+ required: false
2686
+ },
2687
+ {
2688
+ name: 'prop2',
2689
+ type: 'string',
2690
+ required: true,
2691
+ if: {
2692
+ prop1: {
2693
+ min: 0,
2694
+ max: 10
2695
+ }
2696
+ }
2697
+ }
2698
+ ]
2699
+ });
2700
+ const output = {};
2701
+ await apos.schema.convert(req, schema, {
2702
+ prop1: -1,
2703
+ prop2: ''
2704
+ }, output);
2705
+ assert(output.prop2 === '');
2706
+ });
2707
+
2708
+ it('should error required property number min', async function() {
2709
+ const schema = apos.schema.compose({
2710
+ addFields: [
2711
+ {
2712
+ name: 'prop1',
2713
+ type: 'integer',
2714
+ required: false
2715
+ },
2716
+ {
2717
+ name: 'prop2',
2718
+ type: 'string',
2719
+ required: true,
2720
+ if: {
2721
+ prop1: {
2722
+ min: 0,
2723
+ max: 10
2724
+ }
2725
+ }
2726
+ }
2727
+ ]
2728
+ });
2729
+ await testSchemaError(schema, {
2730
+ prop1: 0,
2731
+ prop2: ''
2732
+ }, 'prop2', 'required');
2733
+ });
2734
+
2735
+ it('should enforce required property number max', async function() {
2736
+ const req = apos.task.getReq();
2737
+ const schema = apos.schema.compose({
2738
+ addFields: [
2739
+ {
2740
+ name: 'prop1',
2741
+ type: 'integer',
2742
+ required: false
2743
+ },
2744
+ {
2745
+ name: 'prop2',
2746
+ type: 'string',
2747
+ required: true,
2748
+ if: {
2749
+ prop1: {
2750
+ min: -10,
2751
+ max: 0
2752
+ }
2753
+ }
2754
+ }
2755
+ ]
2756
+ });
2757
+ const output = {};
2758
+ await apos.schema.convert(req, schema, {
2759
+ prop1: 1,
2760
+ prop2: ''
2761
+ }, output);
2762
+ assert(output.prop2 === '');
2763
+ });
2764
+
2765
+ it('should error required property number max', async function() {
2766
+ const schema = apos.schema.compose({
2767
+ addFields: [
2768
+ {
2769
+ name: 'prop1',
2770
+ type: 'integer',
2771
+ required: false
2772
+ },
2773
+ {
2774
+ name: 'prop2',
2775
+ type: 'string',
2776
+ required: true,
2777
+ if: {
2778
+ prop1: {
2779
+ min: -10,
2780
+ max: 0
2781
+ }
2782
+ }
2783
+ }
2784
+ ]
2785
+ });
2786
+ await testSchemaError(schema, {
2787
+ prop1: 0,
2788
+ prop2: ''
2789
+ }, 'prop2', 'required');
2790
+ });
2791
+
2679
2792
  it('should enforce required property nested logical AND', async function() {
2680
2793
  const req = apos.task.getReq();
2681
2794
  const schema = apos.schema.compose({
@@ -2956,48 +3069,49 @@ describe('Schemas', function() {
2956
3069
  const schema = apos.schema.compose({
2957
3070
  addFields: [
2958
3071
  {
2959
- name: 'age',
2960
- type: 'integer',
3072
+ name: 'prop1',
3073
+ type: 'boolean',
2961
3074
  required: false
2962
3075
  },
2963
3076
  {
2964
- name: 'shoeSize',
2965
- type: 'integer',
3077
+ name: 'prop2',
3078
+ type: 'string',
2966
3079
  requiredIf: {
2967
- age: true
3080
+ prop1: true
2968
3081
  }
2969
3082
  }
2970
3083
  ]
2971
3084
  });
2972
3085
  const output = {};
2973
3086
  await apos.schema.convert(req, schema, {
2974
- shoeSize: '',
2975
- age: ''
3087
+ prop1: false,
3088
+ prop2: ''
2976
3089
  }, output);
2977
- assert(output.shoeSize === null);
3090
+
3091
+ assert(output.prop2 === '');
2978
3092
  });
2979
3093
 
2980
3094
  it('should error required property with ifRequired boolean', async function() {
2981
3095
  const schema = apos.schema.compose({
2982
3096
  addFields: [
2983
3097
  {
2984
- name: 'age',
2985
- type: 'integer',
3098
+ name: 'prop1',
3099
+ type: 'boolean',
2986
3100
  required: false
2987
3101
  },
2988
3102
  {
2989
- name: 'shoeSize',
2990
- type: 'integer',
3103
+ name: 'prop2',
3104
+ type: 'string',
2991
3105
  requiredIf: {
2992
- age: true
3106
+ prop1: true
2993
3107
  }
2994
3108
  }
2995
3109
  ]
2996
3110
  });
2997
3111
  await testSchemaError(schema, {
2998
- shoeSize: '',
2999
- age: '18'
3000
- }, 'shoeSize', 'required');
3112
+ prop1: true,
3113
+ prop2: ''
3114
+ }, 'prop2', 'required');
3001
3115
  });
3002
3116
 
3003
3117
  it('should enforce required property with ifRequired string', async function() {
@@ -3006,7 +3120,7 @@ describe('Schemas', function() {
3006
3120
  addFields: [
3007
3121
  {
3008
3122
  name: 'age',
3009
- type: 'integer',
3123
+ type: 'string',
3010
3124
  required: false
3011
3125
  },
3012
3126
  {
@@ -3031,7 +3145,7 @@ describe('Schemas', function() {
3031
3145
  addFields: [
3032
3146
  {
3033
3147
  name: 'age',
3034
- type: 'integer',
3148
+ type: 'string',
3035
3149
  required: false
3036
3150
  },
3037
3151
  {
@@ -3135,8 +3249,29 @@ describe('Schemas', function() {
3135
3249
  required: false
3136
3250
  },
3137
3251
  {
3138
- name: 'prop2',
3139
- type: 'boolean',
3252
+ name: 'shoeSize',
3253
+ type: 'integer',
3254
+ requiredIf: {
3255
+ age: {
3256
+ min: 18
3257
+ }
3258
+ }
3259
+ }
3260
+ ]
3261
+ });
3262
+ await testSchemaError(schema, {
3263
+ shoeSize: '',
3264
+ age: 19
3265
+ }, 'shoeSize', 'required');
3266
+ });
3267
+
3268
+ it('should enforce required property with ifRequired number min 0', async function() {
3269
+ const req = apos.task.getReq();
3270
+ const schema = apos.schema.compose({
3271
+ addFields: [
3272
+ {
3273
+ name: 'age',
3274
+ type: 'integer',
3140
3275
  required: false
3141
3276
  },
3142
3277
  {
@@ -3144,7 +3279,34 @@ describe('Schemas', function() {
3144
3279
  type: 'integer',
3145
3280
  requiredIf: {
3146
3281
  age: {
3147
- min: 18
3282
+ min: 1
3283
+ }
3284
+ }
3285
+ }
3286
+ ]
3287
+ });
3288
+ const output = {};
3289
+ await apos.schema.convert(req, schema, {
3290
+ shoeSize: '',
3291
+ age: 0
3292
+ }, output);
3293
+ assert(output.shoeSize === null);
3294
+ });
3295
+
3296
+ it('should error required property with ifRequired number min 0', async function() {
3297
+ const schema = apos.schema.compose({
3298
+ addFields: [
3299
+ {
3300
+ name: 'age',
3301
+ type: 'integer',
3302
+ required: false
3303
+ },
3304
+ {
3305
+ name: 'shoeSize',
3306
+ type: 'integer',
3307
+ requiredIf: {
3308
+ age: {
3309
+ min: 0
3148
3310
  }
3149
3311
  }
3150
3312
  }
@@ -3152,8 +3314,7 @@ describe('Schemas', function() {
3152
3314
  });
3153
3315
  await testSchemaError(schema, {
3154
3316
  shoeSize: '',
3155
- age: 19,
3156
- prop2: false
3317
+ age: 0
3157
3318
  }, 'shoeSize', 'required');
3158
3319
  });
3159
3320
 
@@ -3194,11 +3355,6 @@ describe('Schemas', function() {
3194
3355
  type: 'integer',
3195
3356
  required: false
3196
3357
  },
3197
- {
3198
- name: 'prop2',
3199
- type: 'boolean',
3200
- required: false
3201
- },
3202
3358
  {
3203
3359
  name: 'shoeSize',
3204
3360
  type: 'integer',
@@ -3217,6 +3373,61 @@ describe('Schemas', function() {
3217
3373
  }, 'shoeSize', 'required');
3218
3374
  });
3219
3375
 
3376
+ it('should enforce required property with ifRequired number max 0', async function() {
3377
+ const req = apos.task.getReq();
3378
+ const schema = apos.schema.compose({
3379
+ addFields: [
3380
+ {
3381
+ name: 'prop1',
3382
+ type: 'integer',
3383
+ required: false
3384
+ },
3385
+ {
3386
+ name: 'prop2',
3387
+ type: 'string',
3388
+ requiredIf: {
3389
+ prop1: {
3390
+ min: -10,
3391
+ max: 0
3392
+ }
3393
+ }
3394
+ }
3395
+ ]
3396
+ });
3397
+ const output = {};
3398
+ await apos.schema.convert(req, schema, {
3399
+ prop1: 1,
3400
+ prop2: ''
3401
+ }, output);
3402
+ assert(output.prop2 === '');
3403
+ });
3404
+
3405
+ it('should error required property with ifRequired number max 0', async function() {
3406
+ const schema = apos.schema.compose({
3407
+ addFields: [
3408
+ {
3409
+ name: 'prop1',
3410
+ type: 'integer',
3411
+ required: false
3412
+ },
3413
+ {
3414
+ name: 'prop2',
3415
+ type: 'string',
3416
+ requiredIf: {
3417
+ prop1: {
3418
+ min: -10,
3419
+ max: 0
3420
+ }
3421
+ }
3422
+ }
3423
+ ]
3424
+ });
3425
+ await testSchemaError(schema, {
3426
+ prop1: 0,
3427
+ prop2: ''
3428
+ }, 'prop2', 'required');
3429
+ });
3430
+
3220
3431
  it('should enforce required property with ifRequired logical AND', async function() {
3221
3432
  const req = apos.task.getReq();
3222
3433
  const schema = apos.schema.compose({
@@ -4080,7 +4291,6 @@ describe('Schemas', function() {
4080
4291
 
4081
4292
  await testSchemaError(schema, {}, 'age', 'required');
4082
4293
  });
4083
-
4084
4294
  });
4085
4295
  });
4086
4296