chore: leverages versions refactor to simplify draft query logic

This commit is contained in:
James
2023-01-16 21:40:34 -05:00
parent dff840c49b
commit 34582da561
10 changed files with 112 additions and 235 deletions

View File

@@ -3,6 +3,7 @@ import paginate from 'mongoose-paginate-v2';
import express from 'express';
import passport from 'passport';
import passportLocalMongoose from 'passport-local-mongoose';
import mongooseAggregatePaginate from 'mongoose-aggregate-paginate-v2';
import { buildVersionCollectionFields } from '../versions/buildCollectionFields';
import buildQueryPlugin from '../mongoose/buildQuery';
import apiKeyStrategy from '../auth/strategies/apiKey';
@@ -82,6 +83,10 @@ export default function registerCollections(ctx: Payload): void {
versionSchema.plugin(paginate, { useEstimatedCount: true })
.plugin(buildQueryPlugin);
if (collection.versions?.drafts) {
versionSchema.plugin(mongooseAggregatePaginate);
}
ctx.versions[collection.slug] = mongoose.model(versionModelName, versionSchema) as CollectionModel;
}

View File

@@ -228,6 +228,7 @@ async function create(incomingArgs: Arguments): Promise<Document> {
id: result.id,
docWithLocales: result,
autosave,
createdAt: result.createdAt,
});
}

View File

@@ -9,7 +9,7 @@ import flattenWhereConstraints from '../../utilities/flattenWhereConstraints';
import { buildSortParam } from '../../mongoose/buildSortParam';
import { AccessResult } from '../../config/types';
import { afterRead } from '../../fields/hooks/afterRead';
import { mergeDrafts } from '../../versions/drafts/mergeDrafts';
import { queryDrafts } from '../../versions/drafts/queryDrafts';
export type Arguments = {
collection: Collection
@@ -160,13 +160,12 @@ async function find<T extends TypeWithID = any>(incomingArgs: Arguments): Promis
};
if (collectionConfig.versions?.drafts && draftsEnabled) {
result = await mergeDrafts({
result = await queryDrafts({
accessResult,
collection,
locale,
paginationOptions,
payload,
query,
where,
});
} else {

View File

@@ -254,6 +254,7 @@ async function update(incomingArgs: Arguments): Promise<Document> {
id,
autosave,
draft: shouldSaveDraft,
createdAt: result.createdAt as string,
});
}

View File

@@ -212,6 +212,7 @@ async function update<T extends TypeWithID = any>(args: Args): Promise<T> {
docWithLocales: result,
autosave,
draft: shouldSaveDraft,
createdAt: global.createdAt,
});
}

View File

@@ -1,229 +0,0 @@
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 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>> => {
// Query the main collection for any IDs that match the query
// Create object "map" for performant lookup
const mainCollectionMatchMap = await collection.Model.find(query, { updatedAt: 1 }, { limit: paginationOptions.limit, sort: paginationOptions.sort })
.lean().then((res) => res.reduce((map, { _id, updatedAt }) => {
const newMap = map;
newMap[_id] = updatedAt;
return newMap;
}, {}));
// 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 includedParentIDs: (string | number)[] = [];
// Create version "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 = await VersionModel.aggregate<AggregateVersion<T>>([
{
$sort: Object.entries(paginationOptions.sort).reduce((sort, [key, order]) => {
return {
...sort,
[key]: order === 'asc' ? 1 : -1,
};
}, {}),
},
{
$group: {
_id: '$parent',
versionID: { $first: '$_id' },
version: { $first: '$version' },
updatedAt: { $first: '$updatedAt' },
createdAt: { $first: '$createdAt' },
},
},
{
$addFields: {
id: {
$toObjectId: '$_id',
},
},
},
{
$lookup: {
from: collection.config.slug,
localField: 'id',
foreignField: '_id',
as: 'parent',
},
},
{
$match: {
parent: {
$size: 1,
},
},
},
{ $match: versionQuery },
{ $limit: paginationOptions.limit },
]).then((res) => res.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;
}, {}));
// Now we need to explicitly exclude any parent matches that have newer versions
// which did NOT appear in the versions query
const excludedParentIDs = 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
const versionsQuery = await VersionModel.find({
updatedAt: {
$gt: parentDocUpdatedAt,
},
parent: {
$eq: parentDocID,
},
}, {}, { limit: 1 }).lean();
// If there are,
// this says that the newest version does not match the incoming query,
// and the parent ID should be excluded
if (versionsQuery.length > 0) {
return parentDocID;
}
return null;
})).then((res) => res.filter((result) => Boolean(result)));
// Run a final query against the main collection,
// passing in any ids to exclude and include
// so that they appear properly paginated
const finalQueryToBuild: { where: Where } = {
where: {
and: [],
},
};
finalQueryToBuild.where.and.push({ or: [] });
if (hasWhereAccessResult(accessResult)) {
finalQueryToBuild.where.and.push(accessResult);
}
if (incomingWhere) {
finalQueryToBuild.where.and[0].or.push(incomingWhere);
}
if (includedParentIDs.length > 0) {
finalQueryToBuild.where.and[0].or.push({
id: {
in: includedParentIDs,
},
});
}
if (excludedParentIDs.length > 0) {
finalQueryToBuild.where.and.push({
id: {
not_in: excludedParentIDs,
},
});
}
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) => {
const matchedVersion = versionCollectionMatchMap[doc.id];
if (matchedVersion && matchedVersion.updatedAt > doc.updatedAt) {
return {
...doc,
...matchedVersion.version,
createdAt: matchedVersion.createdAt,
updatedAt: matchedVersion.updatedAt,
};
}
return replaceWithDraftIfAvailable({
accessResult,
payload,
entity: collection.config,
entityType: 'collection',
doc,
locale,
});
})),
};
return result;
};

View File

@@ -0,0 +1,86 @@
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';
type AggregateVersion<T> = {
_id: string
version: T
updatedAt: string
createdAt: string
}
type Args = {
accessResult: AccessResult
collection: Collection
locale: string
paginationOptions: any
payload: Payload
where: Where
}
export const queryDrafts = async <T extends TypeWithID>({
accessResult,
collection,
locale,
payload,
paginationOptions,
where: incomingWhere,
}: Args): Promise<PaginatedDocs<T>> => {
const VersionModel = payload.versions[collection.config.slug] as CollectionModel;
const where = appendVersionToQueryKey(incomingWhere || {});
const versionQueryToBuild: { where: Where } = {
where: {
...where,
and: [
...where?.and || [],
],
},
};
if (hasWhereAccessResult(accessResult)) {
const versionAccessResult = appendVersionToQueryKey(accessResult);
versionQueryToBuild.where.and.push(versionAccessResult);
}
const versionQuery = await VersionModel.buildQuery(versionQueryToBuild, locale);
const aggregate = VersionModel.aggregate<AggregateVersion<T>>([
{
$sort: Object.entries(paginationOptions.sort).reduce((sort, [key, order]) => {
return {
...sort,
[key]: order === 'asc' ? 1 : -1,
};
}, {}),
},
{
$group: {
_id: '$parent',
versionID: { $first: '$_id' },
version: { $first: '$version' },
updatedAt: { $first: '$updatedAt' },
createdAt: { $first: '$createdAt' },
},
},
{ $match: versionQuery },
{ $limit: paginationOptions.limit },
]);
const result = await VersionModel.aggregatePaginate(aggregate, paginationOptions);
return {
...result,
docs: result.docs.map((doc) => ({
_id: doc._id,
...doc.version,
updatedAt: doc.updatedAt,
createdAt: doc.createdAt,
})),
};
};

View File

@@ -14,6 +14,7 @@ type Args = {
id?: string | number
autosave?: boolean
draft?: boolean
createdAt?: string
}
export const saveVersion = async ({
@@ -24,6 +25,7 @@ export const saveVersion = async ({
docWithLocales,
autosave,
draft,
createdAt,
}: Args): Promise<void> => {
let entityConfig;
let entityType: 'global' | 'collection';
@@ -54,13 +56,17 @@ export const saveVersion = async ({
try {
if (autosave && existingAutosaveVersion?.autosave === true) {
const data: Record<string, unknown> = {
version: versionData,
};
if (createdAt) data.updatedAt = createdAt;
await VersionModel.findByIdAndUpdate(
{
_id: existingAutosaveVersion._id,
},
{
version: versionData,
},
data,
{ new: true, lean: true },
);
// Otherwise, create a new one
@@ -70,6 +76,7 @@ export const saveVersion = async ({
autosave: Boolean(autosave),
};
if (createdAt) data.createdAt = createdAt;
if (collection) data.parent = id;
await VersionModel.create(data);