Files
payload/src/versions/drafts/mergeDrafts.ts

223 lines
6.5 KiB
TypeScript

import { AccessResult } from '../../config/types';
import { Where } from '../../types';
import { Payload } from '../..';
import { PaginatedDocs } from '../../mongoose/types';
import { Collection, CollectionModel, TypeWithID } from '../../collections/config/types';
import { hasWhereAccessResult } from '../../auth';
import { appendVersionToQueryKey } from './appendVersionToQueryKey';
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields';
import replaceWithDraftIfAvailable from './replaceWithDraftIfAvailable';
type AggregateVersion<T> = {
_id: string
version: T
updatedAt: string
createdAt: string
}
type VersionCollectionMatchMap<T> = {
[_id: string | number]: {
updatedAt: string
createdAt: string
version: T
}
}
type Args = {
accessResult: AccessResult
collection: Collection
locale: string
paginationOptions: any
payload: Payload
query: Record<string, unknown>
where: Where
}
export const mergeDrafts = async <T extends TypeWithID>({
accessResult,
collection,
locale,
payload,
paginationOptions,
query,
where: incomingWhere,
}: Args): Promise<PaginatedDocs<T>> => {
console.log('---------STARTING---------');
// Query the main collection, non-paginated, for any IDs that match the query
const mainCollectionFind = await collection.Model.find(query, { updatedAt: 1 }, { limit: paginationOptions.limit }).lean();
// Create object "map" for performant lookup
const mainCollectionMatchMap = mainCollectionFind.reduce((map, { _id, updatedAt }) => {
const newMap = map;
newMap[_id] = updatedAt;
return newMap;
}, {});
console.log({ mainCollectionMatchMap });
// Query the versions collection with a version-specific query
const VersionModel = payload.versions[collection.config.slug] as CollectionModel;
const where = appendVersionToQueryKey(incomingWhere || {});
const versionQueryToBuild: { where: Where } = {
where: {
...where,
and: [
...where?.and || [],
{
'version._status': {
equals: 'draft',
},
},
],
},
};
if (hasWhereAccessResult(accessResult)) {
const versionAccessResult = appendVersionToQueryKey(accessResult);
versionQueryToBuild.where.and.push(versionAccessResult);
}
const versionQuery = await VersionModel.buildQuery(versionQueryToBuild, locale);
const versionCollectionQuery = await VersionModel.aggregate<AggregateVersion<T>>([
{ $sort: { updatedAt: -1 } },
{
$group: {
_id: '$parent',
versionID: { $first: '$_id' },
version: { $first: '$version' },
updatedAt: { $first: '$updatedAt' },
createdAt: { $first: '$createdAt' },
},
},
{ $match: versionQuery },
{ $limit: paginationOptions.limit },
]);
const includedParentIDs: (string | number)[] = [];
// Create object "map" for performant lookup
// and in the same loop, check if there are matched versions without a matched parent
// This means that the newer version's parent should appear in the main query.
// To do so, add the version's parent ID into an explicit `includedIDs` array
const versionCollectionMatchMap = versionCollectionQuery.reduce<VersionCollectionMatchMap<T>>((map, { _id, updatedAt, createdAt, version }) => {
const newMap = map;
newMap[_id] = { version, updatedAt, createdAt };
const matchedParent = mainCollectionMatchMap[_id];
if (!matchedParent) includedParentIDs.push(_id);
return newMap;
}, {});
console.log({ versionCollectionMatchMap });
console.log({ includedParentIDs });
// Now we need to explicitly exclude any parent matches that have newer versions
// which did NOT appear in the versions query
const parentsWithoutNewerVersions = await Promise.all(Object.entries(mainCollectionMatchMap).map(async ([parentDocID, parentDocUpdatedAt]) => {
// If there is a matched version, and it's newer, this parent should remain
if (versionCollectionMatchMap[parentDocID] && versionCollectionMatchMap[parentDocID].updatedAt > parentDocUpdatedAt) {
return null;
}
// Otherwise, we need to check if there are newer versions present
// that did not get returned from the versions query. If there are,
// this says that the newest version does not match the incoming query,
// and the parent ID should be excluded
const versionsQuery = await VersionModel.find({
updatedAt: {
$gt: parentDocUpdatedAt,
},
}, {}, { limit: 1 }).lean();
if (versionsQuery.length > 0) {
return parentDocID;
}
return null;
}));
console.log({ parentsWithoutNewerVersions });
const excludedParentIDs = parentsWithoutNewerVersions.filter((result) => Boolean(result));
console.log({ excludedParentIDs });
// Run a final query against the main collection,
// passing in the ids to exclude and include
// so that they appear correctly
// in paginated results, properly paginated
const finalQueryToBuild: { where: Where } = {
where: {
and: [],
},
};
finalQueryToBuild.where.and.push({ or: [] });
if (hasWhereAccessResult(accessResult)) {
finalQueryToBuild.where.and.push(accessResult);
}
if (where) {
finalQueryToBuild.where.and[0].or.push(where);
}
if (includedParentIDs.length > 0) {
finalQueryToBuild.where.and[0].or.push({
id: {
in: includedParentIDs,
},
});
}
if (excludedParentIDs.length > 0) {
finalQueryToBuild.where.and[0].or.push({
id: {
not_in: excludedParentIDs,
},
});
}
console.log({ finalPayloadQuery: JSON.stringify(finalQueryToBuild, null, 2) });
const finalQuery = await collection.Model.buildQuery(finalQueryToBuild, locale);
let result = await collection.Model.paginate(finalQuery, paginationOptions);
result = {
...result,
docs: await Promise.all(result.docs.map(async (doc) => {
let sanitizedDoc = JSON.parse(JSON.stringify(doc));
sanitizedDoc.id = sanitizedDoc._id;
sanitizedDoc = sanitizeInternalFields(sanitizedDoc);
const matchedVersion = versionCollectionMatchMap[sanitizedDoc.id];
if (matchedVersion) {
return {
...sanitizedDoc,
...matchedVersion.version,
createdAt: matchedVersion.createdAt,
updatedAt: matchedVersion.updatedAt,
};
}
return replaceWithDraftIfAvailable({
accessResult,
payload,
entity: collection.config,
entityType: 'collection',
doc: sanitizedDoc,
locale,
});
})),
};
return result;
};