UNPKG

apostrophe

Version:
1,785 lines (1,540 loc) 54.5 kB
const { strict: assert } = require('node:assert'); const _ = require('lodash'); const t = require('../test-lib/test.js'); describe('Docs', function() { const apiKey = 'this is a test api key'; let apos; this.timeout(t.timeout); after(async function() { await apos.doc.db.deleteMany({}); await apos.lock.db.deleteMany({}); return t.destroy(apos); }); before(async function() { apos = await t.create({ root: module, modules: { '@apostrophecms/express': { options: { apiKeys: { [apiKey]: { role: 'admin' } } } }, 'test-people': { extend: '@apostrophecms/piece-type', fields: { add: { _friends: { type: 'relationship', max: 1, withType: 'test-people', label: 'Friends' } } } }, unlocalized: { extend: '@apostrophecms/piece-type', options: { localized: false }, fields: { add: { _friends: { type: 'relationship', max: 1, withType: 'test-people', label: 'Friends' } } } }, '@apostrophecms/page': { options: { park: [], types: [ { name: 'test-page', label: 'Test Page' } ] } }, 'test-page': { extend: '@apostrophecms/page-type' } } }); }); afterEach(async function () { await apos.doc.db.deleteMany({ type: 'test-people' }); await apos.doc.db.deleteMany({ type: 'test-page' }); await apos.lock.db.deleteMany({}); }); it('should have a db property', function() { assert(apos.doc.db); }); it('should make sure all of the expected indexes are configured', async function() { const expectedIndexes = [ 'type', 'slug', 'titleSortified' ]; const actualIndexes = []; const info = await apos.doc.db.indexInformation(); // Extract the actual index info we care about. _.forEach(info, function(index) { actualIndexes.push(index[0][0]); }); // Now make sure everything in expectedIndexes is in actualIndexes. _.forEach(expectedIndexes, function(index) { assert(_.includes(actualIndexes, index)); }); // Lastly, make sure there is a text index present assert(info.highSearchText_text_lowSearchText_text_title_text_searchBoost_text[0][1] === 'text'); }); it('should be able to fetch schema relationships', async function() { await insertPeople(apos); const manager = apos.doc.getManager('test-people'); const req = apos.task.getAnonReq(); assert(manager); assert(manager.find); assert(manager.schema); const cursor = await manager.find(req, { slug: 'carl' }); assert(cursor); const person = await cursor.toObject(); assert(person); assert(person.slug === 'carl'); assert(person._friends); assert(person._friends[0].slug === 'larry'); }); it('should support custom context menu (legacy, required only)', async function() { const operation = { context: 'update', action: 'test', label: 'Menu Label', modal: 'SomeModalComponent' }; const initialLength = apos.doc.contextOperations.length; apos.doc.addContextOperation('test-people', operation); assert.strictEqual(apos.doc.contextOperations.length, initialLength + 1); assert.deepStrictEqual(apos.doc.contextOperations.find(op => op.action === 'test'), { ...operation, moduleName: 'test-people' }); }); it('should support custom context menu (legacy, with optional)', async function() { apos.doc.contextOperations = []; const operation = { context: 'update', action: 'test', label: 'Menu Label', modal: 'SomeModalComponent', manuallyPublished: true, modifiers: [ 'danger' ] }; assert.strictEqual(apos.doc.contextOperations.length, 0); apos.doc.addContextOperation('test-people', operation); assert.strictEqual(apos.doc.contextOperations.length, 1); assert.deepStrictEqual(apos.doc.contextOperations[0], { ...operation, moduleName: 'test-people' }); }); it('should support custom context menu (modern, required only)', async function() { const operation = { context: 'update', action: 'test2', label: 'Menu Label', modal: 'SomeModalComponent' }; const initialLength = apos.doc.contextOperations.length; apos.doc.addContextOperation(operation); assert.strictEqual(apos.doc.contextOperations.length, initialLength + 1); // Front end is responsible for inferring moduleName here assert.deepStrictEqual(apos.doc.contextOperations.find(op => op.action === 'test2'), operation); }); it('should support custom context menu (modern, with optional)', async function() { apos.doc.contextOperations = []; const operation = { context: 'update', action: 'test', label: 'Menu Label', modal: 'SomeModalComponent', manuallyPublished: true, modifiers: [ 'danger' ] }; assert.strictEqual(apos.doc.contextOperations.length, 0); apos.doc.addContextOperation(operation); assert.strictEqual(apos.doc.contextOperations.length, 1); // Front end is responsible for inferring moduleName here assert.deepStrictEqual(apos.doc.contextOperations[0], operation); }); it('should override custom context menu', async function() { apos.doc.contextOperations = []; const operation1 = { context: 'update', action: 'test', label: 'Op1', modal: 'SomeModalComponent' }; const operation2 = { context: 'update', action: 'test', label: 'Op2', modal: 'SomeModalComponent' }; assert.strictEqual(apos.doc.contextOperations.length, 0); apos.doc.addContextOperation('test-people', operation1); apos.doc.addContextOperation('test-people', operation2); assert.strictEqual(apos.doc.contextOperations.length, 1); assert.deepStrictEqual(apos.doc.contextOperations[0], { ...operation2, moduleName: 'test-people' }); }); /// /// // UNIQUENESS /// /// it('should fail if you try to insert a document with the same unique key twice', async function() { try { await apos.doc.db.insertMany([ { _id: 'peter:en:published', aposDocId: 'peter', aposLocale: 'en:published', type: 'test-people', visibility: 'loginRequired', age: 70, slug: 'peter' }, // ids will not conflict, but slug will { _id: 'peter2:en:published', aposDocId: 'peter2', aposLocale: 'en:published', type: 'test-people', visibility: 'loginRequired', age: 70, slug: 'peter' } ]); assert(false); } catch (e) { assert(e); assert(e.code === 11000); } }); /// /// // FINDING /// /// it('should have a find method on docs that returns a query', function() { const query = apos.doc.find(apos.task.getAnonReq()); assert(query); assert(query.toArray); }); it('should be able to find all test documents and output them as an array', async function () { await insertPeople(apos); const cursor = apos.doc.find(apos.task.getAnonReq(), { type: 'test-people' }); const docs = await cursor.toArray(); // There should be only 3 results. assert(docs.length === 3); // They should all have a type of test-people assert(docs[0].type === 'test-people'); }); /// /// // PROJECTIONS /// /// it('should be able to specify which fields to get by passing a projection object', async function() { await insertPeople(apos); const cursor = apos.doc.find(apos.task.getAnonReq(), { type: 'test-people' }, { project: { age: 1 } }); const docs = await cursor.toArray(); // There SHOULD be an age assert(docs[0].age); // There SHOULD NOT be a firstName assert(!docs[0].firstName); }); /// /// // SORTING /// /// it('should be able to sort', async function () { await insertPeople(apos); const cursor = apos.doc.find(apos.task.getAnonReq(), { type: 'test-people' }).sort({ age: 1 }); const docs = await cursor.toArray(); assert(docs[0].slug === 'larry'); }); it('should be able to sort by multiple keys', async function () { await insertPeople(apos); const cursor = apos.doc.find(apos.task.getAnonReq(), { type: 'test-people' }).sort({ firstName: 1, age: 1 }); const docs = await cursor.toArray(); assert(docs[0].slug === 'carl'); assert(docs[1].slug === 'larry'); }); /// /// // INSERTING /// /// it('should be able to insert a new object into the docs collection in the database', async function() { const response = await insertOne(apos); assert(response); assert(response._id); assert(response._id.endsWith(':en:published')); assert(response._id === `${response.aposDocId}:${response.aposLocale}`); // Direct insertion in published locale should autocreate // a corresponding draft for internal consistency const draft = await apos.doc.db.findOne({ _id: `${response.aposDocId}:en:draft` }); assert(draft); // Unique index allows for duplicates across locales assert(draft.slug === 'one'); // Content properties coming through assert(draft.firstName === response.firstName); const cursor = apos.doc.find(apos.task.getReq(), { type: 'test-people', slug: 'one' }); const docs = await cursor.toArray(); assert(docs[0].slug === 'one'); }); it('should append the slug property with a numeral if inserting an object whose slug already exists in the database', async function() { await insertOne(apos); const object = { slug: 'one', visibility: 'public', type: 'test-people', firstName: 'Harry', lastName: 'Gerber', age: 29, alive: true }; const doc = await apos.doc.insert(apos.task.getReq(), object); assert(doc); assert(doc.slug.match(/^one\d+$/)); }); it('should add the aposDocId to the related documents\' relatedReverseIds field and update their `cacheInvalidatedAt` field', async function() { await insertPeople(apos); const response = await insertOneWithRelated(apos); const carlDoc = await apos.doc.db.findOne({ slug: 'carl', aposLocale: 'en:published' }); const larryDoc = await apos.doc.db.findOne({ slug: 'larry', aposLocale: 'en:published' }); assert(carlDoc.relatedReverseIds.length === 1); assert(carlDoc.relatedReverseIds[0] === 'paul'); assert(carlDoc.cacheInvalidatedAt.getTime() === response.updatedAt.getTime()); assert(larryDoc.relatedReverseIds.length === 1); assert(larryDoc.relatedReverseIds[0] === 'paul'); assert(larryDoc.cacheInvalidatedAt.getTime() === response.updatedAt.getTime()); apos.doc.db.deleteMany({ slug: { $in: [ 'paul', 'carl', 'larry' ] } }); }); it('should remove the related reverse IDs when you delete a draft document', async function () { const req = apos.task.getReq(); await insertPeople(apos); const personWithRelatedPublished = await insertOneWithRelated(apos); const personWithRelatedDraft = await apos.doc.find(apos.task.getReq({ mode: 'draft' }), { slug: 'paul' }).toObject(); await apos.doc.delete(req, personWithRelatedPublished); const larryDoc = await apos.doc.db.findOne({ slug: 'larry', aposLocale: 'en:published' }); const carlDoc = await apos.doc.db.findOne({ slug: 'carl', aposLocale: 'en:published' }); await apos.doc.delete(req, personWithRelatedDraft); const larryDocUpdated = await apos.doc.db.findOne({ slug: 'larry', aposLocale: 'en:published' }); const carlDocUpdated = await apos.doc.db.findOne({ slug: 'carl', aposLocale: 'en:published' }); const actual = { larryHasRelatedBeforeDelete: larryDoc.relatedReverseIds.includes('paul'), carlHasRelatedBeforeDelete: carlDoc.relatedReverseIds.includes('paul'), larryHasRelatedAfterDelete: larryDocUpdated.relatedReverseIds.includes('paul'), carlHasRelatedAfterDelete: carlDocUpdated.relatedReverseIds.includes('paul') }; const expected = { larryHasRelatedBeforeDelete: true, carlHasRelatedBeforeDelete: true, larryHasRelatedAfterDelete: false, carlHasRelatedAfterDelete: false }; assert.deepEqual(actual, expected); }); it('should remove the related reverse IDs when you delete an unlocalized document', async function () { const req = apos.task.getReq(); await insertPeople(apos); const unlocalizedDoc = await apos.doc.insert(req, { aposDocId: 'paul', aposLocale: 'en:published', slug: 'paul', visibility: 'public', type: 'unlocalized', friendsIds: [ 'carl', 'larry' ], _friends: [ { _id: 'carl:en:published' }, { _id: 'larry:en:published' } ] }); const larryDoc = await apos.doc.db.findOne({ slug: 'larry', aposLocale: 'en:published' }); const carlDoc = await apos.doc.db.findOne({ aposDocId: 'carl', aposLocale: 'en:published' }); await apos.doc.delete(req, unlocalizedDoc); const larryDocUpdated = await apos.doc.db.findOne({ slug: 'larry', aposLocale: 'en:published' }); const carlDocUpdated = await apos.doc.db.findOne({ slug: 'carl', aposLocale: 'en:published' }); const actual = { larryHasRelatedBeforeDelete: larryDoc.relatedReverseIds.includes('paul'), carlHasRelatedBeforeDelete: carlDoc.relatedReverseIds.includes('paul'), larryHasRelatedAfterDelete: larryDocUpdated.relatedReverseIds.includes('paul'), carlHasRelatedAfterDelete: carlDocUpdated.relatedReverseIds.includes('paul') }; const expected = { larryHasRelatedBeforeDelete: true, carlHasRelatedBeforeDelete: true, larryHasRelatedAfterDelete: false, carlHasRelatedAfterDelete: false }; assert.deepEqual(actual, expected); }); it('should not allow you to call the insert method if you are not an admin', async function() { const object = { slug: 'not-for-you', visibility: 'loginRequired', type: 'test-people', firstName: 'Darry', lastName: 'Derrber', age: 5, alive: true }; try { await apos.doc.insert(apos.task.getAnonReq(), object); assert(false); } catch (e) { assert(e); } }); /// /// // UPDATING /// /// it('should have an "update" method on docs that updates an existing database object', async function() { await insertOne(apos); const req = apos.task.getReq(); const docs = await apos.doc.find(req, { slug: 'one' }).toArray(); // We should have one document in our results. assert(docs); assert(docs.length === 1); // Grab the object and update the `alive` property. const object = docs[0]; object.alive = false; const updated = await apos.doc.update(apos.task.getReq(), object); // Has the property been updated? assert(updated); assert(updated.alive === false); }); it('should append an updated slug with a numeral if the updated slug already exists', async function() { await insertPeople(apos); await insertOne(apos); const req = apos.task.getReq(); const cursor = apos.doc.find(req, { type: 'test-people', slug: 'one' }); const doc = await cursor.toObject(); assert(doc); doc.slug = 'peter'; const updated = await apos.doc.update(req, doc); assert(updated); // Has the updated slug been appended? assert(updated.slug.match(/^peter\d+$/)); }); it('should be able to fetch all unique firstNames with toDistinct', async function() { await insertPeople(apos); const firstNames = await apos.doc.find(apos.task.getReq(), { type: 'test-people' }).toDistinct('firstName'); assert(Array.isArray(firstNames)); assert(firstNames.length === 4); assert(_.includes(firstNames, 'Larry')); }); it('should be able to fetch all unique firstNames and their counts with toDistinct and distinctCounts', async function() { await insertPeople(apos); const req = apos.task.getReq(); await apos.doc.db.insertOne({ _id: 'random:en:published', slug: 'random', aposDocId: 'lori2', aposLocale: 'en:published', type: 'test-people', visibility: 'loginRequired', firstName: 'Lori', lastName: 'Lora', age: 70 }); const cursor = apos.doc.find(req, { type: 'test-people' }).distinctCounts(true); const firstNames = await cursor.toDistinct('firstName'); assert(Array.isArray(firstNames)); assert(firstNames.length === 4); assert(_.includes(firstNames, 'Larry')); const counts = await cursor.get('distinctCounts'); assert(counts.Larry === 1); assert(counts.Lori === 2); }); it('should remove the aposDocId from the related documents\' relatedReverseIds field and update their `cacheInvalidatedAt` field', async function() { await insertPeople(apos); await insertOneWithRelated(apos); const paulDoc = await apos.doc.db.findOne({ slug: 'paul', aposLocale: 'en:published' }); // carl removed from paul's related friends, only larry remains const object = { ...paulDoc, friendsIds: [ 'larry' ], _friends: [ { _id: 'larry:en:published' } ] }; const response = await apos.doc.update(apos.task.getReq(), object); const carlDoc = await apos.doc.db.findOne({ slug: 'carl', aposLocale: 'en:published' }); const larryDoc = await apos.doc.db.findOne({ slug: 'larry', aposLocale: 'en:published' }); assert(carlDoc.relatedReverseIds.length === 0); assert(carlDoc.cacheInvalidatedAt.getTime() === response.updatedAt.getTime()); assert(larryDoc.relatedReverseIds.length === 1); assert(larryDoc.relatedReverseIds[0] === 'paul'); assert(larryDoc.cacheInvalidatedAt.getTime() === response.updatedAt.getTime()); }); it('should update the related reverse documents\' `cacheInvalidatedAt` field', async function() { await insertPeople(apos); const object = { aposDocId: 'john', aposLocale: 'en:published', slug: 'john', visibility: 'public', type: 'test-people', firstName: 'John', lastName: 'McClane', age: 40, alive: true, friendsIds: [ 'carl' ], _friends: [ { _id: 'carl:en:published' } ] }; await apos.doc.insert(apos.task.getReq(), object); const carlDoc = await apos.doc.db.findOne({ slug: 'carl', aposLocale: 'en:published' }); // update carl, now john (related reverse friend) should have its // `cacheInvalidatedAt` field updated as well const response = await apos.doc.update(apos.task.getReq(), { ...carlDoc, alive: false }); const johnDoc = await apos.doc.db.findOne({ slug: 'john', aposLocale: 'en:published' }); assert(johnDoc.cacheInvalidatedAt.getTime() === response.updatedAt.getTime()); }); it('should update the pieces parent page\'s `cacheInvalidatedAt` field', async function() { const page = { slug: '/parent/new-page', visibility: 'public', type: 'test-page', title: 'New Page' }; const object = { aposDocId: 'bruce', aposLocale: 'en:published', slug: 'bruce', visibility: 'public', type: 'test-people', firstName: 'Bruce', lastName: 'Lee', age: 30, alive: false, _parentSlug: '/parent/new-page' }; await apos.doc.insert(apos.task.getReq(), page); const response = await apos.doc.insert(apos.task.getReq(), object); const pageDoc = await apos.doc.db.findOne({ slug: '/parent/new-page', aposLocale: 'en:published' }); assert(pageDoc.cacheInvalidatedAt.getTime() === response.updatedAt.getTime()); }); it('should not allow you to call the update method if you are not an admin', async function() { const cursor = apos.doc.find(apos.task.getAnonReq(), { type: 'test-people', slug: 'lori' }); const doc = cursor.toObject(); assert(doc); doc.slug = 'laurie'; try { await apos.doc.update(apos.task.getAnonReq(), doc); assert(false); } catch (e) { assert(e); } }); /// /// // ARCHIVE /// /// it('should archive docs by updating them', async function() { await insertPeople(apos); const req = apos.task.getReq(); const doc = await apos.doc.find(req, { type: 'test-people', slug: 'carl' }).toObject(); const archived = await archiveDoc(apos, doc); assert(archived.archived === true); }); it('should not be able to find the archived object', async function() { const req = apos.task.getReq(); await insertPeople(apos); const carlDoc = await apos.doc.find(req, { type: 'test-people', slug: 'carl' }).toObject(); await archiveDoc(apos, carlDoc); const doc = await apos.doc.find(req, { slug: 'carl' }).toObject(); assert(!doc); }); it('should not allow you to call the archive method if you are not an admin', async function() { try { await apos.doc.archived(apos.task.getAnonReq(), { slug: 'lori' }); assert(false); } catch (e) { assert(e); } }); it('should be able to find the archived object when using the "archived" method on find()', async function() { const req = apos.task.getReq(); await insertPeople(apos); const carlDoc = await apos.doc.find(req, { type: 'test-people', slug: 'carl' }).toObject(); await archiveDoc(apos, carlDoc); // Look for the archived doc with the `deduplicate-` + its `_id` + its // `name` properties. const doc = await apos.doc.find(req, { slug: 'deduplicate-carl-carl' }).archived(true).toObject(); assert(doc); assert(doc.archived); }); /// /// // RESCUE /// /// it('should rescue a doc by updating the "archived" property from an object', async function() { const req = apos.task.getReq(); await insertPeople(apos); const carlDoc = await apos.doc.find(req, { type: 'test-people', slug: 'carl' }).toObject(); await archiveDoc(apos, carlDoc); const doc = await apos.doc.find(req, { slug: 'deduplicate-carl-carl' }).archived(null).toObject(); await apos.doc.update(req, { ...doc, archived: false }); const newDoc = await apos.doc.find(req, { slug: 'carl' }).toObject(); // We should have a document. assert(newDoc); assert(newDoc.slug === 'carl'); assert(newDoc.archived === false); }); it('should not allow you to call the restore method if you are not an admin', async function() { try { await insertPeople(apos); const carlDoc = await apos.doc.find(apos.task.getReq(), { type: 'test-people', slug: 'carl' }).toObject(); await archiveDoc(apos, carlDoc); await apos.doc.restore(apos.task.getAnonReq(), { slug: 'carl' }); assert(false); } catch (e) { assert(e); } }); it('should throw an exception on find() if you fail to pass req as the first argument', async function() { try { await apos.doc.find({ slug: 'larry' }); assert(false); } catch (e) { assert(e); } }); it('should respect _ids()', async function() { const testItems = []; let i; for (i = 0; (i < 100); i++) { testItems.push({ _id: `i${i}:en:published`, aposDocId: `i${i}`, aposLocale: 'en:published', slug: `i${i}`, visibility: 'public', type: 'test', title: 'title: ' + i }); } await apos.doc.db.insertMany(testItems); const docs = await apos.doc.find(apos.task.getAnonReq(), {}) ._ids([ 'i7:en:published', 'i3:en:published', 'i27:en:published', 'i9:en:published' ]).toArray(); assert(docs[0]._id === 'i7:en:published'); assert(docs[0].aposDocId === 'i7'); assert(docs[0].aposLocale === 'en:published'); assert(docs[1]._id === 'i3:en:published'); assert(docs[2]._id === 'i27:en:published'); assert(docs[3]._id === 'i9:en:published'); assert(!docs[4]); const filteredDocs = await apos.doc.find(apos.task.getAnonReq(), {}) ._ids([ 'i7:en:published', 'i3:en:published', 'i27:en:published', 'i9:en:published' ]).skip(2).limit(2).toArray(); assert(filteredDocs[0]._id === 'i27:en:published'); assert(filteredDocs[1]._id === 'i9:en:published'); assert(!filteredDocs[2]); }); it('should be able to lock a document', async function() { const req = apos.task.getReq(); await insertPeople(apos); const doc = await apos.doc.db.findOne({ _id: 'carl:en:published' }); try { await apos.doc.lock(req, doc, 'abc'); } catch (e) { assert(!e); } }); it('should not be able to lock a document with a different tabId', async function() { const req = apos.task.getReq(); await insertPeople(apos); const doc = await apos.doc.db.findOne({ _id: 'carl:en:published' }); try { await apos.doc.lock(req, doc, 'abc'); const locked = await apos.doc.db.findOne({ _id: 'carl:en:published' }); await apos.doc.lock(req, locked, 'def'); } catch (e) { assert(e); assert(e.name === 'locked'); } }); it('should be able to refresh the lock with the same tabId', async function() { const req = apos.task.getReq(); await insertPeople(apos); const doc = await apos.doc.db.findOne({ _id: 'carl:en:published' }); const wait = (time) => new Promise((resolve) => setTimeout(() => { resolve(); }, time)); try { await apos.doc.lock(req, doc, 'abc'); const locked = await apos.doc.db.findOne({ _id: 'carl:en:published' }); await wait(500); await apos.doc.lock(req, locked, 'abc'); } catch (e) { assert(!e); } }); it('should be able to unlock a document', async function() { const req = apos.task.getReq(); await insertPeople(apos); const doc = await apos.doc.db.findOne({ _id: 'carl:en:published' }); try { await apos.doc.lock(req, doc, 'abc'); await apos.doc.unlock(req, doc, 'abc'); } catch (e) { assert(false); } }); it('should be able to re-lock an unlocked document', async function() { const req = apos.task.getReq(); await insertPeople(apos); const doc = await apos.doc.db.findOne({ _id: 'carl:en:published' }); try { await apos.doc.lock(req, doc, 'abc'); await apos.doc.unlock(req, doc, 'abc'); await apos.doc.lock(req, doc, 'def'); } catch (e) { assert(false); } }); it('should be able to lock a locked document with force: true', async function() { const req = apos.task.getReq(); await insertPeople(apos); const doc = await apos.doc.db.findOne({ _id: 'carl:en:published' }); try { await apos.doc.lock(req, doc, 'def'); await apos.doc.lock(req, doc, 'abc', { force: true }); } catch (e) { assert(false); } }); it('should be able to recover if the text index weights are mysteriously wrong at startup', async function() { await apos.doc.db.dropIndex('highSearchText_text_lowSearchText_text_title_text_searchBoost_text'); await apos.doc.db.createIndex({ highSearchText: 'text', lowSearchText: 'text', title: 'text', searchBoost: 'text' }, { default_language: 'none', weights: { // These are the weird weights we've seen when this // mystery bug crops up, flunking createIndex on a // later startup title: 1, searchBoost: 1, highSearchText: 1, lowSearchText: 1 } }); await apos.doc.createTextIndex(); }); /// /// // MIGRATIONS /// /// it('should add via a migration the `cacheInvalidatedAt` field to any doc and set it to equal the doc\'s `updatedAt` field', async function() { const objects = [ { slug: 'test-for-cacheInvalidatedAt-field-migration1', visibility: 'public', type: 'test-people', firstName: 'Kurt', lastName: 'Cobain', age: 27, alive: false, updatedAt: '2022-03-28T12:57:03.685Z' }, { slug: 'test-for-cacheInvalidatedAt-field-migration2', visibility: 'public', type: 'test-people', firstName: 'Jim', lastName: 'Morrison', age: 27, alive: false, updatedAt: '2020-08-29T12:57:03.685Z' } ]; await apos.doc.db.insertMany(objects); await apos.doc.setCacheField(); const docs = await apos.doc.db.find({ slug: /test-for-cacheInvalidatedAt-field-migration/ }).toArray(); docs.forEach((doc, index) => { const timestamps = { doc: new Date(doc.cacheInvalidatedAt).toString(), expected: new Date(objects[index].updatedAt).toString() }; assert(timestamps.doc === timestamps.expected); }); }); it('should not add via a migration the `cacheInvalidatedAt` field to docs that already have it', async function() { const object = { slug: 'test-for-cacheInvalidatedAt-field-migration3', visibility: 'public', type: 'test-people', firstName: 'Janis', lastName: 'Joplin', age: 27, alive: false, updatedAt: '2018-08-29T12:57:03.685Z', cacheInvalidatedAt: '2019-08-29T12:57:03.685Z' }; await apos.doc.db.insert(object); await apos.doc.setCacheField(); const doc = await apos.doc.db.findOne({ slug: 'test-for-cacheInvalidatedAt-field-migration3' }); const timestamps = { doc: new Date(doc.cacheInvalidatedAt).toString(), expected: new Date(object.cacheInvalidatedAt).toString() }; assert(timestamps.doc === timestamps.expected); }); it('should preserve latin accents by default (piece)', async function () { const req = apos.task.getReq(); const object = { title: 'C\'est déjà l\'été', visibility: 'public', type: 'test-people', firstName: 'Janis', lastName: 'Joplin', age: 27, alive: false, updatedAt: '2018-08-29T12:57:03.685Z', cacheInvalidatedAt: '2019-08-29T12:57:03.685Z' }; await apos.doc.insert(req, object); const doc = await apos.doc.db.findOne({ title: 'C\'est déjà l\'été' }); assert.equal(doc.slug, 'c-est-déjà-l-été'); }); it('should remove latin accents when configured to do so (piece)', async function () { const req = apos.task.getReq(); const object = { title: 'C\'est déjà l\'été', visibility: 'public', type: 'test-people', firstName: 'Janis', lastName: 'Joplin', age: 27, alive: false, updatedAt: '2018-08-29T12:57:03.685Z', cacheInvalidatedAt: '2019-08-29T12:57:03.685Z' }; const originalSetting = apos.i18n.options.stripUrlAccents; apos.i18n.options.stripUrlAccents = true; await apos.doc.insert(req, object); const doc = await apos.doc.db.findOne({ title: 'C\'est déjà l\'été' }); apos.i18n.options.stripUrlAccents = originalSetting; assert.equal(doc.slug, 'c-est-deja-l-ete'); }); it('should remove latin accents when converting schema fields (piece)', async function () { const req = apos.task.getReq(); const input = { title: 'C\'est déjà l\'été', slug: 'c-est-déjà-l-été', visibility: 'public', type: 'test-people', firstName: 'Janis', lastName: 'Joplin', age: 27, alive: false, updatedAt: '2018-08-29T12:57:03.685Z', cacheInvalidatedAt: '2019-08-29T12:57:03.685Z' }; const originalSetting = apos.i18n.options.stripUrlAccents; apos.i18n.options.stripUrlAccents = true; const manager = apos.doc.getManager('test-people'); const page = manager.newInstance(); await manager.convert(req, input, page); apos.i18n.options.stripUrlAccents = originalSetting; assert.equal(page.slug, 'c-est-deja-l-ete'); }); it('should preserve latin accents by default (page)', async function () { const req = apos.task.getReq(); const object = { title: 'C\'est déjà l\'été', visibility: 'public', type: 'test-page' }; await apos.doc.insert(req, object); const doc = await apos.doc.db.findOne({ title: 'C\'est déjà l\'été' }); assert.equal(doc.slug, '/c-est-déjà-l-été'); }); it('should remove latin accents when configured to do so (page)', async function () { const req = apos.task.getReq(); const object = { title: 'C\'est déjà l\'été', visibility: 'public', type: 'test-page' }; const originalSetting = apos.i18n.options.stripUrlAccents; apos.i18n.options.stripUrlAccents = true; await apos.doc.insert(req, object); const doc = await apos.doc.db.findOne({ title: 'C\'est déjà l\'été' }); apos.i18n.options.stripUrlAccents = originalSetting; assert.equal(doc.slug, '/c-est-deja-l-ete'); }); it('should remove latin accents when converting schema fields (page)', async function () { const req = apos.task.getReq(); const input = { title: 'C\'est déjà l\'été', slug: '/c-est-déjà-l-été', visibility: 'public', type: 'test-page' }; const originalSetting = apos.i18n.options.stripUrlAccents; apos.i18n.options.stripUrlAccents = true; const manager = apos.doc.getManager('test-page'); const page = manager.newInstance(); await manager.convert(req, input, page); apos.i18n.options.stripUrlAccents = originalSetting; assert.equal(page.slug, '/c-est-deja-l-ete'); }); /// /// // CACHING /// /// it('should add a `cacheInvalidatedAt` field and set it to equal `updatedAt` field when saving a doc', async function() { const object = { slug: 'test-for-cacheInvalidatedAt-field', visibility: 'public', type: 'test-people', firstName: 'Michael', lastName: 'Jackson', age: 64, alive: true }; const response = await apos.doc.insert(apos.task.getReq(), object); const draft = await apos.doc.db.findOne({ _id: `${response.aposDocId}:en:draft` }); assert(response.cacheInvalidatedAt.getTime() === response.updatedAt.getTime()); assert(draft.cacheInvalidatedAt.getTime() === draft.updatedAt.getTime()); }); describe('beforeInsert handler', function() { it('should rely on req.mode when inserting a doc without _id', async function() { const req = apos.task.getReq(); const draftReq = apos.task.getReq({ mode: 'draft' }); const people = apos.modules['test-people']; const instance = people.newInstance(); const piece1 = { ...instance, title: 'piece 1' }; const piece2 = { ...instance, title: 'piece 2' }; const pieceDraft = await people.insert(draftReq, piece1); const piecePublished = await people.insert(req, piece2); const actual = { draft: { idMode: pieceDraft._id.split(':').pop(), aposLocale: pieceDraft.aposLocale, aposMode: pieceDraft.aposMode }, published: { idMode: piecePublished._id.split(':').pop(), aposLocale: piecePublished.aposLocale, aposMode: piecePublished.aposMode } }; const expected = { draft: { idMode: 'draft', aposLocale: 'en:draft', aposMode: 'draft' }, published: { idMode: 'published', aposLocale: 'en:published', aposMode: 'published' } }; assert.deepEqual(actual, expected); }); it('should rely on _id when present for aposMode and aposLocale even if req.mode does not match', async function() { const req = apos.task.getReq(); const draftReq = apos.task.getReq({ mode: 'draft' }); const people = apos.modules['test-people']; const instance = people.newInstance(); const piece1 = { _id: 'testid:en:draft', ...instance, title: 'piece 1' }; const piece2 = { _id: 'testid:en:published', ...instance, title: 'piece 2' }; const pieceDraft = await people.insert(req, piece1); const piecePublished = await people.insert(draftReq, piece2); const actual = { draft: { idMode: pieceDraft._id.split(':').pop(), aposLocale: pieceDraft.aposLocale, aposMode: pieceDraft.aposMode }, published: { idMode: piecePublished._id.split(':').pop(), aposLocale: piecePublished.aposLocale, aposMode: piecePublished.aposMode } }; const expected = { draft: { idMode: 'draft', aposLocale: 'en:draft', aposMode: 'draft' }, published: { idMode: 'published', aposLocale: 'en:published', aposMode: 'published' } }; assert.deepEqual(actual, expected); }); it('should rely on aposMode when present for _id and aposLocale even if req.mode does not match', async function() { const req = apos.task.getReq(); const draftReq = apos.task.getReq({ mode: 'draft' }); const people = apos.modules['test-people']; const instance = people.newInstance(); const piece1 = { ...instance, title: 'piece 1', aposLocale: 'en:draft' }; const piece2 = { ...instance, title: 'piece 2', aposLocale: 'en:published' }; const pieceDraft = await people.insert(req, piece1); const piecePublished = await people.insert(draftReq, piece2); const actual = { draft: { idMode: pieceDraft._id.split(':').pop(), aposLocale: pieceDraft.aposLocale, aposMode: pieceDraft.aposMode }, published: { idMode: piecePublished._id.split(':').pop(), aposLocale: piecePublished.aposLocale, aposMode: piecePublished.aposMode } }; const expected = { draft: { idMode: 'draft', aposLocale: 'en:draft', aposMode: 'draft' }, published: { idMode: 'published', aposLocale: 'en:published', aposMode: 'published' } }; assert.deepEqual(actual, expected); }); }); describe('beforeUnpublish handler', function() { it('should prevent un-publishing of the global doc', async function() { const req = apos.task.getReq(); const global = await apos.doc.find(req, { type: '@apostrophecms/global' }).toObject(); try { await apos.http.post( `/api/v1/@apostrophecms/global/${global._id}/unpublish?apiKey=${apiKey}`, { body: {}, busy: true } ); } catch (error) { assert(error.status === 403); return; } throw new Error('Should have thrown a forbidden error (should not be able to unpublish the global doc)'); }); }); }); describe('Docs: tasks', function () { let apos; this.timeout(t.timeout); before(async function() { apos = await t.create({ root: module, modules: { '@apostrophecms/i18n': { options: { locales: { en: {}, fr: { prefix: '/fr' } } } }, 'test-people': { extend: '@apostrophecms/piece-type', fields: { add: { _friends: { type: 'relationship', withType: 'test-people', label: 'Friends' } } } }, unlocalized: { extend: '@apostrophecms/piece-type', options: { localized: false }, fields: { add: { _friends: { type: 'relationship', max: 1, withType: 'test-people', label: 'Friends' } } } }, '@apostrophecms/page': { options: { park: [], types: [ { name: 'test-page', label: 'Test Page' } ] } }, 'test-page': { extend: '@apostrophecms/page-type' } } }); }); after(async function() { await t.destroy(apos); }); beforeEach(async function() { await apos.doc.db.deleteMany({}); await apos.lock.db.deleteMany({}); }); it('should require the _id or slug when calling @apostrophecms/doc:get-apos-doc-id task', async function() { await insertI18nFixtures(apos); const actual = async () => { await apos.task.invoke( '@apostrophecms/doc:get-apos-doc-id', { locale: 'fr' } ); }; const expected = { message: 'Either _id or slug must be provided', name: 'invalid' }; await assert.rejects(actual, expected); }); it('should not require the locale when calling @apostrophecms/doc:get-apos-doc-id task with _id', async function() { await insertI18nFixtures(apos); const req = apos.task.getReq({ locale: 'en' }); const doc = await apos.doc.find(req, { type: 'test-people', slug: 'carl' }).toObject(); const actual = await apos.task.invoke( '@apostrophecms/doc:get-apos-doc-id', { _id: doc._id } ); const expected = doc.aposDocId; assert.equal(actual, expected); }); it('should require the locale when calling @apostrophecms/doc:get-apos-doc-id task with slug', async function() { await insertI18nFixtures(apos); const req = apos.task.getReq({ locale: 'en' }); const doc = await apos.doc.find(req, { type: 'test-people', slug: 'carl' }).toObject(); const actual = async () => { await apos.task.invoke( '@apostrophecms/doc:get-apos-doc-id', { slug: doc.slug } ); }; const expected = { message: 'Missing locale', name: 'invalid' }; await assert.rejects(actual, expected); }); it('should get the aposDocId when calling @apostrophecms/doc:get-apos-doc-id task with a slug', async function() { await insertI18nFixtures(apos); const req = apos.task.getReq({ locale: 'en' }); const doc = await apos.doc.find(req, { type: 'test-people', slug: 'carl' }).toObject(); const actual = await apos.task.invoke( '@apostrophecms/doc:get-apos-doc-id', { slug: doc.slug, locale: 'en' } ); const expected = doc.aposDocId; assert.equal(actual, expected); }); it('should get the aposDocId when calling @apostrophecms/doc:get-apos-doc-id task with an _id', async function() { await insertI18nFixtures(apos); const req = apos.task.getReq({ locale: 'en' }); const doc = await apos.doc.find(req, { type: 'test-people', slug: 'carl' }).toObject(); const actual = await apos.task.invoke( '@apostrophecms/doc:get-apos-doc-id', { _id: doc._id, locale: 'en' } ); const expected = doc.aposDocId; assert.equal(actual, expected); }); it('should require a new ID when calling @apostrophecms/doc:set-apos-doc-id task', async function() { await insertI18nFixtures(apos); const req = apos.task.getReq({ locale: 'fr' }); const doc = await apos.doc.find(req, { type: 'test-people', slug: 'carl' }).toObject(); const actual = async () => { await apos.task.invoke( '@apostrophecms/doc:set-apos-doc-id', { 'old-id': doc.aposDocId, locale: 'fr' } ); }; const expected = { message: 'Missing newId', name: 'invalid' }; await assert.rejects(actual, expected); }); it('should require an old-id or slug when calling @apostrophecms/doc:set-apos-doc-id task', async function() { await insertI18nFixtures(apos); const actual = async () => { await apos.task.invoke( '@apostrophecms/doc:set-apos-doc-id', { 'new-id': 'carl', locale: 'fr' } ); }; const expected = { message: 'Either oldId or slug must be provided', name: 'invalid' }; await assert.rejects(actual, expected); }); it('should require a locale when calling @apostrophecms/doc:set-apos-doc-id task', async function() { await insertI18nFixtures(apos); const req = apos.task.getReq({ locale: 'fr' }); const doc = await apos.doc.find(req, { type: 'test-people', slug: 'carl' }).toObject(); const actual = async () => { await apos.task.invoke( '@apostrophecms/doc:set-apos-doc-id', { 'new-id': 'carl', 'old-id': doc.aposDocId } ); }; const expected = { message: 'Missing locale', name: 'invalid' }; await assert.rejects(actual, expected); }); it('should update the aposDocId when calling @apostrophecms/doc:set-apos-doc-id task with a slug and a new aposDocId', async function() { await insertI18nFixtures(apos); const req = apos.task.getReq({ locale: 'fr' }); const doc = await apos.doc.find(req, { type: 'test-people', slug: 'carl' }).toObject(); await apos.task.invoke( '@apostrophecms/doc:set-apos-doc-id', { 'new-id': 'carl', slug: doc.slug, locale: 'fr' } ); const paul = await apos.doc.find( apos.task.getReq({ locale: 'fr' }), { slug: 'paul' } ).toObject(); const carl = await apos.doc.find( apos.task.getReq({ locale: 'fr' }), { slug: 'carl' } ).toObject(); const actual = { carl: { _id: carl._id, aposDocId: carl.aposDocId, aposLocale: carl.aposLocale }, paul: { friendsIds: paul.friendsIds } }; const expected = { carl: { _id: 'carl:fr:published', aposDocId: 'carl', aposLocale: 'fr:published' }, paul: { friendsIds: [ 'carl', 'test-people-larry' ] } }; assert.deepEqual(actual, expected); }); it('should update the aposDocId when calling @apostrophecms/doc:set-apos-doc-id task with an old and new aposDocId', async function() { await insertI18nFixtures(apos); const req = apos.task.getReq({ locale: 'fr' }); const doc = await apos.doc.find(req, { type: 'test-people', slug: 'carl' }).toObject(); await apos.task.invoke( '@apostrophecms/doc:set-apos-doc-id', { 'new-id': 'carl', 'old-id': doc.aposDocId, locale: 'fr' } ); const paul = await apos.doc.find( apos.task.getReq({ locale: 'fr' }), { slug: 'paul' } ).toObject(); const carl = await apos.doc.find( apos.task.getReq({ locale: 'fr' }), { slug: 'carl' } ).toObject(); const actual = { carl: { _id: carl._id, aposDocId: carl.aposDocId, aposLocale: carl.aposLocale }, paul: { friendsIds: paul.friendsIds } }; const expected = { carl: { _id: 'carl:fr:published', aposDocId: 'carl', aposLocale: 'fr:published' }, paul: { friendsIds: [ 'carl', 'test-people-larry' ] } }; assert.deepEqual(actual, expected); }); }); async function insertPeople(apos) { return apos.doc.db.insertMany([ { _id: 'lori:en:draft', aposDocId: 'lori', aposLocale: 'en:draft', slug: 'lori', visibility: 'public', type: 'test-people', firstName: 'Lori', lastName: 'Pizzaroni', age: 32, alive: true }, { _id: 'larry:en:draft', aposDocId: 'larry', aposLocale: 'en:draft', slug: 'larry', visibility: 'public', type: 'test-people', firstName: 'Larry', lastName: 'Cherber', age: 28, alive: true }, { _id: 'carl:en:draft', aposDocId: 'carl', aposLocale: 'en:draft', slug: 'carl', visibility: 'public', type: 'test-people', firstName: 'Carl', lastName: 'Sagan', age: 62, alive: false, friendsIds: [ 'larry' ] }, { _id: 'peter:en:draft', aposDocId: 'peter', aposLocale: 'en:draft', type: 'test-people', visibility: 'loginRequired', firstName: 'Peter', lastName: 'Pan', age: 70, slug: 'peter' }, { _id: 'lori:en:published', aposDocId: 'lori', aposLocale: 'en:published', slug: 'lori', visibility: 'public', type: 'test-people', firstName: 'Lori', lastName: 'Pizzaroni', age: 32, alive: true }, { _id: 'larry:en:published', aposDocId: 'larry', aposLocale: 'en:published', slug: 'larry', visibility: 'public', type: 'test-people', firstName: 'Larry', lastName: 'Cherber', ag