Merge pull request #2515 from payloadcms/feat/abstract-query-engine
Feat/abstract query engine
This commit is contained in:
@@ -58,6 +58,7 @@ export const DocumentInfoProvider: React.FC<Props> = ({
|
||||
const getVersions = useCallback(async () => {
|
||||
let versionFetchURL;
|
||||
let publishedFetchURL;
|
||||
let draftsEnabled = false;
|
||||
let shouldFetchVersions = false;
|
||||
let unpublishedVersionJSON = null;
|
||||
let versionJSON = null;
|
||||
@@ -93,12 +94,14 @@ export const DocumentInfoProvider: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
if (global) {
|
||||
draftsEnabled = Boolean(global?.versions?.drafts);
|
||||
shouldFetchVersions = Boolean(global?.versions);
|
||||
versionFetchURL = `${baseURL}/globals/${global.slug}/versions`;
|
||||
publishedFetchURL = `${baseURL}/globals/${global.slug}?${qs.stringify(publishedVersionParams)}`;
|
||||
}
|
||||
|
||||
if (collection) {
|
||||
draftsEnabled = Boolean(collection?.versions?.drafts);
|
||||
shouldFetchVersions = Boolean(collection?.versions);
|
||||
versionFetchURL = `${baseURL}/${collection.slug}/versions`;
|
||||
|
||||
@@ -122,15 +125,19 @@ export const DocumentInfoProvider: React.FC<Props> = ({
|
||||
}
|
||||
|
||||
if (shouldFetch) {
|
||||
let publishedJSON = await fetch(publishedFetchURL, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
}).then((res) => res.json());
|
||||
let publishedJSON;
|
||||
|
||||
if (collection) {
|
||||
publishedJSON = publishedJSON?.docs?.[0];
|
||||
if (draftsEnabled) {
|
||||
publishedJSON = await fetch(publishedFetchURL, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
}).then((res) => res.json());
|
||||
|
||||
if (collection) {
|
||||
publishedJSON = publishedJSON?.docs?.[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldFetchVersions) {
|
||||
|
||||
@@ -16,26 +16,24 @@ const getExecuteStaticAccess = ({ config, Model }) => async (req: PayloadRequest
|
||||
if (typeof accessResult === 'object') {
|
||||
const filename = decodeURI(req.path).replace(/^\/|\/$/g, '');
|
||||
|
||||
const queryToBuild: { where: Where } = {
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
or: [
|
||||
{
|
||||
filename: {
|
||||
equals: filename,
|
||||
},
|
||||
const queryToBuild: Where = {
|
||||
and: [
|
||||
{
|
||||
or: [
|
||||
{
|
||||
filename: {
|
||||
equals: filename,
|
||||
},
|
||||
],
|
||||
},
|
||||
accessResult,
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
accessResult,
|
||||
],
|
||||
};
|
||||
|
||||
if (config.upload.imageSizes) {
|
||||
config.upload.imageSizes.forEach(({ name }) => {
|
||||
queryToBuild.where.and[0].or.push({
|
||||
queryToBuild.and[0].or.push({
|
||||
[`sizes.${name}.filename`]: {
|
||||
equals: filename,
|
||||
},
|
||||
@@ -43,7 +41,12 @@ const getExecuteStaticAccess = ({ config, Model }) => async (req: PayloadRequest
|
||||
});
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery(queryToBuild, req.locale);
|
||||
const query = await Model.buildQuery({
|
||||
where: queryToBuild,
|
||||
req,
|
||||
overrideAccess: true,
|
||||
});
|
||||
|
||||
const doc = await Model.findOne(query);
|
||||
|
||||
if (!doc) {
|
||||
|
||||
@@ -45,7 +45,6 @@ export default (payload: Payload, { Model, config }): PassportAPIKey => {
|
||||
},
|
||||
req: req as PayloadRequest,
|
||||
overrideAccess: true,
|
||||
queryHiddenFields: true,
|
||||
depth: config.auth.depth,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import paginate from 'mongoose-paginate-v2';
|
||||
import { Schema } from 'mongoose';
|
||||
import { SanitizedConfig } from '../config/types';
|
||||
import buildQueryPlugin from '../mongoose/buildQuery';
|
||||
import getBuildQueryPlugin from '../mongoose/buildQuery';
|
||||
import buildSchema from '../mongoose/buildSchema';
|
||||
import { SanitizedCollectionConfig } from './config/types';
|
||||
|
||||
@@ -26,7 +26,7 @@ const buildCollectionSchema = (collection: SanitizedCollectionConfig, config: Sa
|
||||
}
|
||||
|
||||
schema.plugin(paginate, { useEstimatedCount: true })
|
||||
.plugin(buildQueryPlugin);
|
||||
.plugin(getBuildQueryPlugin({ collectionSlug: collection.slug }));
|
||||
|
||||
return schema;
|
||||
};
|
||||
|
||||
@@ -10,7 +10,6 @@ import getBaseUploadFields from '../../uploads/getBaseFields';
|
||||
import { formatLabels } from '../../utilities/formatLabels';
|
||||
import { defaults, authDefaults } from './defaults';
|
||||
import { Config } from '../../config/types';
|
||||
import { versionCollectionDefaults } from '../../versions/defaults';
|
||||
import baseVersionFields from '../../versions/baseFields';
|
||||
import TimestampsRequired from '../../errors/TimestampsRequired';
|
||||
import mergeBaseFields from '../../fields/mergeBaseFields';
|
||||
@@ -40,12 +39,14 @@ const sanitizeCollection = (config: Config, collection: CollectionConfig): Sanit
|
||||
};
|
||||
}
|
||||
|
||||
if (sanitized.versions.drafts.autosave === true) sanitized.versions.drafts.autosave = {};
|
||||
if (sanitized.versions.drafts.autosave === true) {
|
||||
sanitized.versions.drafts.autosave = {
|
||||
interval: 2000,
|
||||
};
|
||||
}
|
||||
|
||||
sanitized.fields = mergeBaseFields(sanitized.fields, baseVersionFields);
|
||||
}
|
||||
|
||||
sanitized.versions = merge(versionCollectionDefaults, sanitized.versions);
|
||||
}
|
||||
|
||||
if (sanitized.upload) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Auth, IncomingAuthType, User } from '../../auth/types';
|
||||
import { IncomingUploadType, Upload } from '../../uploads/types';
|
||||
import { IncomingCollectionVersions, SanitizedCollectionVersions } from '../../versions/types';
|
||||
import { Config as GeneratedTypes } from '../../generated-types';
|
||||
import { BuildQueryArgs } from '../../mongoose/buildQuery';
|
||||
|
||||
type Register<T = any> = (doc: T, password: string) => T;
|
||||
|
||||
@@ -19,7 +20,7 @@ interface PassportLocalModel {
|
||||
}
|
||||
|
||||
export interface CollectionModel extends Model<any>, PaginateModel<any>, AggregatePaginateModel<any>, PassportLocalModel {
|
||||
buildQuery: (query: unknown, locale: string, queryHiddenFields?: boolean) => Record<string, unknown>
|
||||
buildQuery: (args: BuildQueryArgs) => Promise<Record<string, unknown>>
|
||||
}
|
||||
|
||||
export interface AuthCollectionModel extends CollectionModel {
|
||||
|
||||
@@ -3,7 +3,7 @@ import paginate from 'mongoose-paginate-v2';
|
||||
import passportLocalMongoose from 'passport-local-mongoose';
|
||||
import mongooseAggregatePaginate from 'mongoose-aggregate-paginate-v2';
|
||||
import { buildVersionCollectionFields } from '../versions/buildCollectionFields';
|
||||
import buildQueryPlugin from '../mongoose/buildQuery';
|
||||
import getBuildQueryPlugin from '../mongoose/buildQuery';
|
||||
import buildCollectionSchema from './buildSchema';
|
||||
import buildSchema from '../mongoose/buildSchema';
|
||||
import { CollectionModel, SanitizedCollectionConfig } from './config/types';
|
||||
@@ -62,9 +62,11 @@ export default function initCollectionsLocal(ctx: Payload): void {
|
||||
if (collection.versions) {
|
||||
const versionModelName = getVersionsModelName(collection);
|
||||
|
||||
const versionCollectionFields = buildVersionCollectionFields(collection);
|
||||
|
||||
const versionSchema = buildSchema(
|
||||
ctx.config,
|
||||
buildVersionCollectionFields(collection),
|
||||
versionCollectionFields,
|
||||
{
|
||||
disableUnique: true,
|
||||
draftsEnabled: true,
|
||||
@@ -76,7 +78,7 @@ export default function initCollectionsLocal(ctx: Payload): void {
|
||||
);
|
||||
|
||||
versionSchema.plugin(paginate, { useEstimatedCount: true })
|
||||
.plugin(buildQueryPlugin);
|
||||
.plugin(getBuildQueryPlugin({ collectionSlug: collection.slug, versionsFields: versionCollectionFields }));
|
||||
|
||||
if (collection.versions?.drafts) {
|
||||
versionSchema.plugin(mongooseAggregatePaginate);
|
||||
|
||||
@@ -74,21 +74,19 @@ async function deleteOperation<TSlug extends keyof GeneratedTypes['collections']
|
||||
// Access
|
||||
// /////////////////////////////////////
|
||||
|
||||
const queryToBuild: { where?: Where } = {
|
||||
where: {
|
||||
and: [],
|
||||
},
|
||||
let queryToBuild: Where = {
|
||||
and: [],
|
||||
};
|
||||
|
||||
if (where) {
|
||||
queryToBuild.where = {
|
||||
queryToBuild = {
|
||||
and: [],
|
||||
...where,
|
||||
};
|
||||
|
||||
if (Array.isArray(where.AND)) {
|
||||
queryToBuild.where.and = [
|
||||
...queryToBuild.where.and,
|
||||
queryToBuild.and = [
|
||||
...queryToBuild.and,
|
||||
...where.AND,
|
||||
];
|
||||
}
|
||||
@@ -100,11 +98,15 @@ async function deleteOperation<TSlug extends keyof GeneratedTypes['collections']
|
||||
accessResult = await executeAccess({ req }, collectionConfig.access.delete);
|
||||
|
||||
if (hasWhereAccessResult(accessResult)) {
|
||||
queryToBuild.where.and.push(accessResult);
|
||||
queryToBuild.and.push(accessResult);
|
||||
}
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery(queryToBuild, locale);
|
||||
const query = await Model.buildQuery({
|
||||
where: queryToBuild,
|
||||
req,
|
||||
overrideAccess,
|
||||
});
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Retrieve documents
|
||||
|
||||
@@ -45,7 +45,6 @@ async function deleteByID<TSlug extends keyof GeneratedTypes['collections']>(inc
|
||||
req,
|
||||
req: {
|
||||
t,
|
||||
locale,
|
||||
payload,
|
||||
payload: {
|
||||
config,
|
||||
@@ -80,25 +79,25 @@ async function deleteByID<TSlug extends keyof GeneratedTypes['collections']>(inc
|
||||
// Retrieve document
|
||||
// /////////////////////////////////////
|
||||
|
||||
const queryToBuild: {
|
||||
where: Where
|
||||
} = {
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
id: {
|
||||
equals: id,
|
||||
},
|
||||
const queryToBuild: Where = {
|
||||
and: [
|
||||
{
|
||||
id: {
|
||||
equals: id,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (hasWhereAccessResult(accessResults)) {
|
||||
(queryToBuild.where.and as Where[]).push(accessResults);
|
||||
queryToBuild.and.push(accessResults);
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery(queryToBuild, locale);
|
||||
const query = await Model.buildQuery({
|
||||
req,
|
||||
where: queryToBuild,
|
||||
overrideAccess,
|
||||
});
|
||||
|
||||
const docToDelete = await Model.findOne(query);
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ export type Arguments = {
|
||||
disableErrors?: boolean
|
||||
pagination?: boolean
|
||||
showHiddenFields?: boolean
|
||||
queryHiddenFields?: boolean
|
||||
draft?: boolean
|
||||
}
|
||||
|
||||
@@ -66,7 +65,6 @@ async function find<T extends TypeWithID & Record<string, unknown>>(
|
||||
overrideAccess,
|
||||
disableErrors,
|
||||
showHiddenFields,
|
||||
queryHiddenFields,
|
||||
pagination = true,
|
||||
} = args;
|
||||
|
||||
@@ -74,23 +72,21 @@ async function find<T extends TypeWithID & Record<string, unknown>>(
|
||||
// Access
|
||||
// /////////////////////////////////////
|
||||
|
||||
const queryToBuild: { where?: Where } = {
|
||||
where: {
|
||||
and: [],
|
||||
},
|
||||
let queryToBuild: Where = {
|
||||
and: [],
|
||||
};
|
||||
|
||||
let useEstimatedCount = false;
|
||||
|
||||
if (where) {
|
||||
queryToBuild.where = {
|
||||
queryToBuild = {
|
||||
and: [],
|
||||
...where,
|
||||
};
|
||||
|
||||
if (Array.isArray(where.AND)) {
|
||||
queryToBuild.where.and = [
|
||||
...queryToBuild.where.and,
|
||||
queryToBuild.and = [
|
||||
...queryToBuild.and,
|
||||
...where.AND,
|
||||
];
|
||||
}
|
||||
@@ -122,11 +118,15 @@ async function find<T extends TypeWithID & Record<string, unknown>>(
|
||||
}
|
||||
|
||||
if (hasWhereAccessResult(accessResult)) {
|
||||
queryToBuild.where.and.push(accessResult);
|
||||
queryToBuild.and.push(accessResult);
|
||||
}
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery(queryToBuild, locale, queryHiddenFields);
|
||||
const query = await Model.buildQuery({
|
||||
req,
|
||||
where: queryToBuild,
|
||||
overrideAccess,
|
||||
});
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Find
|
||||
@@ -166,7 +166,8 @@ async function find<T extends TypeWithID & Record<string, unknown>>(
|
||||
result = await queryDrafts<T>({
|
||||
accessResult,
|
||||
collection,
|
||||
locale,
|
||||
req,
|
||||
overrideAccess,
|
||||
paginationOptions,
|
||||
payload,
|
||||
where,
|
||||
|
||||
@@ -50,7 +50,6 @@ async function findByID<T extends TypeWithID>(
|
||||
req,
|
||||
req: {
|
||||
t,
|
||||
locale,
|
||||
payload,
|
||||
},
|
||||
disableErrors,
|
||||
@@ -69,23 +68,25 @@ async function findByID<T extends TypeWithID>(
|
||||
// If errors are disabled, and access returns false, return null
|
||||
if (accessResult === false) return null;
|
||||
|
||||
const queryToBuild: { where: Where } = {
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
_id: {
|
||||
equals: id,
|
||||
},
|
||||
const queryToBuild: Where = {
|
||||
and: [
|
||||
{
|
||||
_id: {
|
||||
equals: id,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (hasWhereAccessResult(accessResult)) {
|
||||
queryToBuild.where.and.push(accessResult);
|
||||
queryToBuild.and.push(accessResult);
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery(queryToBuild, locale);
|
||||
const query = await Model.buildQuery({
|
||||
where: queryToBuild,
|
||||
req,
|
||||
overrideAccess,
|
||||
});
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Find by ID
|
||||
@@ -132,7 +133,8 @@ async function findByID<T extends TypeWithID>(
|
||||
entityType: 'collection',
|
||||
doc: result,
|
||||
accessResult,
|
||||
locale,
|
||||
req,
|
||||
overrideAccess,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ async function findVersionByID<T extends TypeWithVersion<T> = any>(args: Argumen
|
||||
req,
|
||||
req: {
|
||||
t,
|
||||
locale,
|
||||
payload,
|
||||
},
|
||||
disableErrors,
|
||||
@@ -57,23 +56,25 @@ async function findVersionByID<T extends TypeWithVersion<T> = any>(args: Argumen
|
||||
|
||||
const hasWhereAccess = typeof accessResults === 'object';
|
||||
|
||||
const queryToBuild: { where: Where } = {
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
_id: {
|
||||
equals: id,
|
||||
},
|
||||
const queryToBuild: Where = {
|
||||
and: [
|
||||
{
|
||||
_id: {
|
||||
equals: id,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (hasWhereAccessResult(accessResults)) {
|
||||
(queryToBuild.where.and as Where[]).push(accessResults);
|
||||
queryToBuild.and.push(accessResults);
|
||||
}
|
||||
|
||||
const query = await VersionsModel.buildQuery(queryToBuild, locale);
|
||||
const query = await VersionsModel.buildQuery({
|
||||
where: queryToBuild,
|
||||
req,
|
||||
overrideAccess,
|
||||
});
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Find by ID
|
||||
|
||||
@@ -49,7 +49,7 @@ async function findVersions<T extends TypeWithVersion<T>>(
|
||||
// Access
|
||||
// /////////////////////////////////////
|
||||
|
||||
const queryToBuild: { where?: Where } = {};
|
||||
let queryToBuild: Where = {};
|
||||
let useEstimatedCount = false;
|
||||
|
||||
if (where) {
|
||||
@@ -58,7 +58,7 @@ async function findVersions<T extends TypeWithVersion<T>>(
|
||||
if (Array.isArray(where.and)) and = where.and;
|
||||
if (Array.isArray(where.AND)) and = where.AND;
|
||||
|
||||
queryToBuild.where = {
|
||||
queryToBuild = {
|
||||
...where,
|
||||
and: [
|
||||
...and,
|
||||
@@ -75,18 +75,22 @@ async function findVersions<T extends TypeWithVersion<T>>(
|
||||
|
||||
if (hasWhereAccessResult(accessResults)) {
|
||||
if (!where) {
|
||||
queryToBuild.where = {
|
||||
queryToBuild = {
|
||||
and: [
|
||||
accessResults,
|
||||
],
|
||||
};
|
||||
} else {
|
||||
(queryToBuild.where.and as Where[]).push(accessResults);
|
||||
queryToBuild.and.push(accessResults);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const query = await VersionsModel.buildQuery(queryToBuild, locale);
|
||||
const query = await VersionsModel.buildQuery({
|
||||
where: queryToBuild,
|
||||
req,
|
||||
overrideAccess,
|
||||
});
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Find
|
||||
|
||||
@@ -20,7 +20,6 @@ export type Options<T extends keyof GeneratedTypes['collections']> = {
|
||||
overrideAccess?: boolean
|
||||
disableErrors?: boolean
|
||||
showHiddenFields?: boolean
|
||||
queryHiddenFields?: boolean
|
||||
pagination?: boolean
|
||||
sort?: string
|
||||
where?: Where
|
||||
@@ -45,7 +44,6 @@ export default async function findLocal<T extends keyof GeneratedTypes['collecti
|
||||
overrideAccess = true,
|
||||
disableErrors,
|
||||
showHiddenFields,
|
||||
queryHiddenFields,
|
||||
sort,
|
||||
draft = false,
|
||||
pagination = true,
|
||||
@@ -81,7 +79,6 @@ export default async function findLocal<T extends keyof GeneratedTypes['collecti
|
||||
overrideAccess,
|
||||
disableErrors,
|
||||
showHiddenFields,
|
||||
queryHiddenFields,
|
||||
draft,
|
||||
pagination,
|
||||
req,
|
||||
|
||||
@@ -73,23 +73,25 @@ async function restoreVersion<T extends TypeWithID = any>(args: Arguments): Prom
|
||||
// Retrieve document
|
||||
// /////////////////////////////////////
|
||||
|
||||
const queryToBuild: { where: Where } = {
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
id: {
|
||||
equals: parentDocID,
|
||||
},
|
||||
const queryToBuild: Where = {
|
||||
and: [
|
||||
{
|
||||
id: {
|
||||
equals: parentDocID,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (hasWhereAccessResult(accessResults)) {
|
||||
(queryToBuild.where.and as Where[]).push(accessResults);
|
||||
queryToBuild.and.push(accessResults);
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery(queryToBuild, locale);
|
||||
const query = await Model.buildQuery({
|
||||
where: queryToBuild,
|
||||
req,
|
||||
overrideAccess,
|
||||
});
|
||||
|
||||
const doc = await Model.findOne(query);
|
||||
|
||||
|
||||
@@ -84,21 +84,19 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
// Access
|
||||
// /////////////////////////////////////
|
||||
|
||||
const queryToBuild: { where?: Where } = {
|
||||
where: {
|
||||
and: [],
|
||||
},
|
||||
let queryToBuild: Where = {
|
||||
and: [],
|
||||
};
|
||||
|
||||
if (where) {
|
||||
queryToBuild.where = {
|
||||
queryToBuild = {
|
||||
and: [],
|
||||
...where,
|
||||
};
|
||||
|
||||
if (Array.isArray(where.AND)) {
|
||||
queryToBuild.where.and = [
|
||||
...queryToBuild.where.and,
|
||||
queryToBuild.and = [
|
||||
...queryToBuild.and,
|
||||
...where.AND,
|
||||
];
|
||||
}
|
||||
@@ -110,11 +108,15 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
accessResult = await executeAccess({ req }, collectionConfig.access.update);
|
||||
|
||||
if (hasWhereAccessResult(accessResult)) {
|
||||
queryToBuild.where.and.push(accessResult);
|
||||
queryToBuild.and.push(accessResult);
|
||||
}
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery(queryToBuild, locale);
|
||||
const query = await Model.buildQuery({
|
||||
where: queryToBuild,
|
||||
req,
|
||||
overrideAccess,
|
||||
});
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Retrieve documents
|
||||
@@ -125,7 +127,8 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
docs = await queryDrafts<GeneratedTypes['collections'][TSlug]>({
|
||||
accessResult,
|
||||
collection,
|
||||
locale,
|
||||
req,
|
||||
overrideAccess,
|
||||
payload,
|
||||
where: query,
|
||||
});
|
||||
|
||||
@@ -96,23 +96,25 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
// Retrieve document
|
||||
// /////////////////////////////////////
|
||||
|
||||
const queryToBuild: { where: Where } = {
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
id: {
|
||||
equals: id,
|
||||
},
|
||||
const queryToBuild: Where = {
|
||||
and: [
|
||||
{
|
||||
id: {
|
||||
equals: id,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (hasWhereAccessResult(accessResults)) {
|
||||
(queryToBuild.where.and as Where[]).push(accessResults);
|
||||
queryToBuild.and.push(accessResults);
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery(queryToBuild, locale);
|
||||
const query = await Model.buildQuery({
|
||||
where: queryToBuild,
|
||||
req,
|
||||
overrideAccess,
|
||||
});
|
||||
|
||||
const doc = await getLatestCollectionVersion({
|
||||
payload,
|
||||
|
||||
16
src/errors/QueryError.ts
Normal file
16
src/errors/QueryError.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import httpStatus from 'http-status';
|
||||
import type { TFunction } from 'i18next';
|
||||
import APIError from './APIError';
|
||||
|
||||
class QueryError extends APIError {
|
||||
constructor(results: { path: string }[], t?: TFunction) {
|
||||
const message = t ? t('error:unspecific', { count: results.length }) : `The following path${results.length === 1 ? '' : 's'} cannot be queried:`;
|
||||
super(
|
||||
`${message} ${results.map((err) => err.path).join(', ')}`,
|
||||
httpStatus.BAD_REQUEST,
|
||||
results,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default QueryError;
|
||||
@@ -1,14 +1,14 @@
|
||||
import mongoose from 'mongoose';
|
||||
import buildSchema from '../mongoose/buildSchema';
|
||||
import { SanitizedConfig } from '../config/types';
|
||||
import buildQueryPlugin from '../mongoose/buildQuery';
|
||||
import getBuildQueryPlugin from '../mongoose/buildQuery';
|
||||
import { GlobalModel } from './config/types';
|
||||
|
||||
const buildModel = (config: SanitizedConfig): GlobalModel | null => {
|
||||
if (config.globals && config.globals.length > 0) {
|
||||
const globalsSchema = new mongoose.Schema({}, { discriminatorKey: 'globalType', timestamps: true, minimize: false });
|
||||
|
||||
globalsSchema.plugin(buildQueryPlugin);
|
||||
globalsSchema.plugin(getBuildQueryPlugin());
|
||||
|
||||
const Globals = mongoose.model('globals', globalsSchema) as unknown as GlobalModel;
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { GlobalConfig, SanitizedGlobalConfig } from './types';
|
||||
import defaultAccess from '../../auth/defaultAccess';
|
||||
import baseVersionFields from '../../versions/baseFields';
|
||||
import mergeBaseFields from '../../fields/mergeBaseFields';
|
||||
import { versionGlobalDefaults } from '../../versions/defaults';
|
||||
|
||||
const sanitizeGlobals = (collections: CollectionConfig[], globals: GlobalConfig[]): SanitizedGlobalConfig[] => {
|
||||
const sanitizedGlobals = globals.map((global) => {
|
||||
@@ -42,12 +41,14 @@ const sanitizeGlobals = (collections: CollectionConfig[], globals: GlobalConfig[
|
||||
};
|
||||
}
|
||||
|
||||
if (sanitizedGlobal.versions.drafts.autosave === true) sanitizedGlobal.versions.drafts.autosave = {};
|
||||
if (sanitizedGlobal.versions.drafts.autosave === true) {
|
||||
sanitizedGlobal.versions.drafts.autosave = {
|
||||
interval: 2000,
|
||||
};
|
||||
}
|
||||
|
||||
sanitizedGlobal.fields = mergeBaseFields(sanitizedGlobal.fields, baseVersionFields);
|
||||
}
|
||||
|
||||
sanitizedGlobal.versions = merge(versionGlobalDefaults, sanitizedGlobal.versions);
|
||||
}
|
||||
|
||||
// /////////////////////////////////
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import mongoose from 'mongoose';
|
||||
import paginate from 'mongoose-paginate-v2';
|
||||
import buildQueryPlugin from '../mongoose/buildQuery';
|
||||
import getBuildQueryPlugin from '../mongoose/buildQuery';
|
||||
import buildModel from './buildModel';
|
||||
import { Payload } from '../payload';
|
||||
import { getVersionsModelName } from '../versions/getVersionsModelName';
|
||||
@@ -19,9 +19,11 @@ export default function initGlobalsLocal(ctx: Payload): void {
|
||||
if (global.versions) {
|
||||
const versionModelName = getVersionsModelName(global);
|
||||
|
||||
const versionGlobalFields = buildVersionGlobalFields(global);
|
||||
|
||||
const versionSchema = buildSchema(
|
||||
ctx.config,
|
||||
buildVersionGlobalFields(global),
|
||||
versionGlobalFields,
|
||||
{
|
||||
disableUnique: true,
|
||||
draftsEnabled: true,
|
||||
@@ -33,7 +35,7 @@ export default function initGlobalsLocal(ctx: Payload): void {
|
||||
);
|
||||
|
||||
versionSchema.plugin(paginate, { useEstimatedCount: true })
|
||||
.plugin(buildQueryPlugin);
|
||||
.plugin(getBuildQueryPlugin({ versionsFields: versionGlobalFields }));
|
||||
|
||||
ctx.versions[global.slug] = mongoose.model(versionModelName, versionSchema) as CollectionModel;
|
||||
}
|
||||
|
||||
@@ -40,16 +40,14 @@ async function findOne<T extends Record<string, unknown>>(args: Args): Promise<T
|
||||
// Retrieve and execute access
|
||||
// /////////////////////////////////////
|
||||
|
||||
const queryToBuild: { where?: Where } = {
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
globalType: {
|
||||
equals: slug,
|
||||
},
|
||||
const queryToBuild: Where = {
|
||||
and: [
|
||||
{
|
||||
globalType: {
|
||||
equals: slug,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let accessResult: AccessResult;
|
||||
@@ -58,11 +56,16 @@ async function findOne<T extends Record<string, unknown>>(args: Args): Promise<T
|
||||
accessResult = await executeAccess({ req }, globalConfig.access.read);
|
||||
|
||||
if (hasWhereAccessResult(accessResult)) {
|
||||
queryToBuild.where.and.push(accessResult);
|
||||
queryToBuild.and.push(accessResult);
|
||||
}
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery(queryToBuild, locale);
|
||||
const query = await Model.buildQuery({
|
||||
where: queryToBuild,
|
||||
req,
|
||||
overrideAccess,
|
||||
globalSlug: slug,
|
||||
});
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Perform database operation
|
||||
@@ -91,7 +94,8 @@ async function findOne<T extends Record<string, unknown>>(args: Args): Promise<T
|
||||
entity: globalConfig,
|
||||
entityType: 'global',
|
||||
doc,
|
||||
locale,
|
||||
req,
|
||||
overrideAccess,
|
||||
accessResult,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ async function findVersionByID<T extends TypeWithVersion<T> = any>(args: Argumen
|
||||
req: {
|
||||
t,
|
||||
payload,
|
||||
locale,
|
||||
},
|
||||
disableErrors,
|
||||
currentDepth,
|
||||
@@ -50,23 +49,26 @@ async function findVersionByID<T extends TypeWithVersion<T> = any>(args: Argumen
|
||||
|
||||
const hasWhereAccess = typeof accessResults === 'object';
|
||||
|
||||
const queryToBuild: { where: Where } = {
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
_id: {
|
||||
equals: id,
|
||||
},
|
||||
const queryToBuild: Where = {
|
||||
and: [
|
||||
{
|
||||
_id: {
|
||||
equals: id,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (hasWhereAccessResult(accessResults)) {
|
||||
(queryToBuild.where.and as Where[]).push(accessResults);
|
||||
queryToBuild.and.push(accessResults);
|
||||
}
|
||||
|
||||
const query = await VersionsModel.buildQuery(queryToBuild, locale);
|
||||
const query = await VersionsModel.buildQuery({
|
||||
where: queryToBuild,
|
||||
req,
|
||||
overrideAccess,
|
||||
globalSlug: globalConfig.slug,
|
||||
});
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Find by ID
|
||||
|
||||
@@ -47,7 +47,7 @@ async function findVersions<T extends TypeWithVersion<T>>(
|
||||
// Access
|
||||
// /////////////////////////////////////
|
||||
|
||||
const queryToBuild: { where?: Where } = {};
|
||||
let queryToBuild: Where = {};
|
||||
let useEstimatedCount = false;
|
||||
|
||||
if (where) {
|
||||
@@ -56,7 +56,7 @@ async function findVersions<T extends TypeWithVersion<T>>(
|
||||
if (Array.isArray(where.and)) and = where.and;
|
||||
if (Array.isArray(where.AND)) and = where.AND;
|
||||
|
||||
queryToBuild.where = {
|
||||
queryToBuild = {
|
||||
...where,
|
||||
and: [
|
||||
...and,
|
||||
@@ -73,18 +73,23 @@ async function findVersions<T extends TypeWithVersion<T>>(
|
||||
|
||||
if (hasWhereAccessResult(accessResults)) {
|
||||
if (!where) {
|
||||
queryToBuild.where = {
|
||||
queryToBuild = {
|
||||
and: [
|
||||
accessResults,
|
||||
],
|
||||
};
|
||||
} else {
|
||||
(queryToBuild.where.and as Where[]).push(accessResults);
|
||||
queryToBuild.and.push(accessResults);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const query = await VersionsModel.buildQuery(queryToBuild, locale);
|
||||
const query = await VersionsModel.buildQuery({
|
||||
where: queryToBuild,
|
||||
req,
|
||||
overrideAccess,
|
||||
globalSlug: globalConfig.slug,
|
||||
});
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Find
|
||||
|
||||
@@ -33,7 +33,6 @@ async function update<TSlug extends keyof GeneratedTypes['globals']>(
|
||||
slug,
|
||||
req,
|
||||
req: {
|
||||
locale,
|
||||
payload,
|
||||
payload: {
|
||||
globals: {
|
||||
@@ -62,23 +61,26 @@ async function update<TSlug extends keyof GeneratedTypes['globals']>(
|
||||
// Retrieve document
|
||||
// /////////////////////////////////////
|
||||
|
||||
const queryToBuild: { where: Where } = {
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
globalType: {
|
||||
equals: slug,
|
||||
},
|
||||
const queryToBuild: Where = {
|
||||
and: [
|
||||
{
|
||||
globalType: {
|
||||
equals: slug,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (hasWhereAccessResult(accessResults)) {
|
||||
(queryToBuild.where.and as Where[]).push(accessResults);
|
||||
queryToBuild.and.push(accessResults);
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery(queryToBuild, locale);
|
||||
const query = await Model.buildQuery({
|
||||
where: queryToBuild,
|
||||
req,
|
||||
overrideAccess,
|
||||
globalSlug: slug,
|
||||
});
|
||||
|
||||
// /////////////////////////////////////
|
||||
// 2. Retrieve document
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
/* eslint-disable no-continue */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import deepmerge from 'deepmerge';
|
||||
import mongoose, { FilterQuery } from 'mongoose';
|
||||
import { FilterQuery } from 'mongoose';
|
||||
import { combineMerge } from '../utilities/combineMerge';
|
||||
import { CollectionModel } from '../collections/config/types';
|
||||
import { getSchemaTypeOptions } from './getSchemaTypeOptions';
|
||||
import { operatorMap } from './operatorMap';
|
||||
import { sanitizeQueryValue } from './sanitizeFormattedValue';
|
||||
import { sanitizeQueryValue } from './sanitizeQueryValue';
|
||||
import { PayloadRequest, Where } from '../types';
|
||||
import { Field, FieldAffectingData, fieldAffectsData, TabAsField, UIField } from '../fields/config/types';
|
||||
import { CollectionPermission, FieldPermissions, GlobalPermission } from '../auth';
|
||||
import flattenFields from '../utilities/flattenTopLevelFields';
|
||||
import { getEntityPolicies } from '../utilities/getEntityPolicies';
|
||||
import { SanitizedConfig } from '../config/types';
|
||||
import QueryError from '../errors/QueryError';
|
||||
|
||||
const validOperators = ['like', 'contains', 'in', 'all', 'not_in', 'greater_than_equal', 'greater_than', 'less_than_equal', 'less_than', 'not_equals', 'equals', 'exists', 'near'];
|
||||
|
||||
@@ -15,18 +21,15 @@ const subQueryOptions = {
|
||||
lean: true,
|
||||
};
|
||||
|
||||
type ParseType = {
|
||||
searchParams?:
|
||||
{
|
||||
[key: string]: any;
|
||||
};
|
||||
sort?: boolean;
|
||||
};
|
||||
|
||||
type PathToQuery = {
|
||||
complete: boolean
|
||||
collectionSlug?: string
|
||||
path: string
|
||||
Model: CollectionModel
|
||||
field: Field | TabAsField
|
||||
fields?: (FieldAffectingData | UIField | TabAsField)[]
|
||||
fieldPolicies?: {
|
||||
[field: string]: FieldPermissions
|
||||
}
|
||||
}
|
||||
|
||||
type SearchParam = {
|
||||
@@ -34,51 +37,91 @@ type SearchParam = {
|
||||
value: unknown,
|
||||
}
|
||||
|
||||
class ParamParser {
|
||||
locale: string;
|
||||
type ParamParserArgs = {
|
||||
req: PayloadRequest
|
||||
collectionSlug?: string
|
||||
globalSlug?: string
|
||||
versionsFields?: Field[]
|
||||
model: any
|
||||
where: Where
|
||||
overrideAccess?: boolean
|
||||
}
|
||||
|
||||
queryHiddenFields: boolean
|
||||
export class ParamParser {
|
||||
collectionSlug?: string
|
||||
|
||||
rawParams: any;
|
||||
globalSlug?: string
|
||||
|
||||
overrideAccess: boolean
|
||||
|
||||
req: PayloadRequest
|
||||
|
||||
where: Where;
|
||||
|
||||
model: any;
|
||||
|
||||
query: {
|
||||
searchParams: {
|
||||
[key: string]: any;
|
||||
};
|
||||
sort: boolean;
|
||||
};
|
||||
fields: Field[]
|
||||
|
||||
constructor(model, rawParams, locale: string, queryHiddenFields?: boolean) {
|
||||
localizationConfig: SanitizedConfig['localization']
|
||||
|
||||
policies: {
|
||||
collections?: {
|
||||
[collectionSlug: string]: CollectionPermission;
|
||||
};
|
||||
globals?: {
|
||||
[globalSlug: string]: GlobalPermission;
|
||||
};
|
||||
}
|
||||
|
||||
errors: { path: string }[]
|
||||
|
||||
constructor({
|
||||
req,
|
||||
collectionSlug,
|
||||
globalSlug,
|
||||
versionsFields,
|
||||
model,
|
||||
where,
|
||||
overrideAccess,
|
||||
}: ParamParserArgs) {
|
||||
this.req = req;
|
||||
this.collectionSlug = collectionSlug;
|
||||
this.globalSlug = globalSlug;
|
||||
this.parse = this.parse.bind(this);
|
||||
this.model = model;
|
||||
this.rawParams = rawParams;
|
||||
this.locale = locale;
|
||||
this.queryHiddenFields = queryHiddenFields;
|
||||
this.query = {
|
||||
searchParams: {},
|
||||
sort: false,
|
||||
this.where = where;
|
||||
this.overrideAccess = overrideAccess;
|
||||
this.localizationConfig = req.payload.config.localization;
|
||||
this.policies = {
|
||||
collections: {},
|
||||
globals: {},
|
||||
};
|
||||
this.errors = [];
|
||||
|
||||
// Get entity fields
|
||||
if (globalSlug) {
|
||||
const globalConfig = req.payload.globals.config.find(({ slug }) => slug === globalSlug);
|
||||
this.fields = versionsFields || globalConfig.fields;
|
||||
}
|
||||
|
||||
if (collectionSlug) {
|
||||
const collectionConfig = req.payload.collections[collectionSlug].config;
|
||||
this.fields = versionsFields || collectionConfig.fields;
|
||||
}
|
||||
}
|
||||
|
||||
// Entry point to the ParamParser class
|
||||
|
||||
async parse(): Promise<ParseType> {
|
||||
if (typeof this.rawParams === 'object') {
|
||||
for (const key of Object.keys(this.rawParams)) {
|
||||
if (key === 'where') {
|
||||
this.query.searchParams = await this.parsePathOrRelation(this.rawParams.where);
|
||||
} else if (key === 'sort') {
|
||||
this.query.sort = this.rawParams[key];
|
||||
}
|
||||
}
|
||||
return this.query;
|
||||
async parse(): Promise<Record<string, unknown>> {
|
||||
if (typeof this.where === 'object') {
|
||||
const query = await this.parsePathOrRelation(this.where);
|
||||
return query;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
async parsePathOrRelation(object) {
|
||||
async parsePathOrRelation(object: Where): Promise<Record<string, unknown>> {
|
||||
let result = {} as FilterQuery<any>;
|
||||
// We need to determine if the whereKey is an AND, OR, or a schema path
|
||||
for (const relationOrPath of Object.keys(object)) {
|
||||
@@ -98,7 +141,12 @@ class ParamParser {
|
||||
if (typeof pathOperators === 'object') {
|
||||
for (const operator of Object.keys(pathOperators)) {
|
||||
if (validOperators.includes(operator)) {
|
||||
const searchParam = await this.buildSearchParam(this.model.schema, relationOrPath, pathOperators[operator], operator);
|
||||
const searchParam = await this.buildSearchParam({
|
||||
fields: this.fields,
|
||||
incomingPath: relationOrPath,
|
||||
val: pathOperators[operator],
|
||||
operator,
|
||||
});
|
||||
|
||||
if (searchParam?.value && searchParam?.path) {
|
||||
result = {
|
||||
@@ -132,123 +180,85 @@ class ParamParser {
|
||||
return completedConditions;
|
||||
}
|
||||
|
||||
// Build up an array of auto-localized paths to search on
|
||||
// Multiple paths may be possible if searching on properties of relationship fields
|
||||
|
||||
getLocalizedPaths(Model: CollectionModel, incomingPath: string, operator): PathToQuery[] {
|
||||
const { schema } = Model;
|
||||
const pathSegments = incomingPath.split('.');
|
||||
|
||||
let paths: PathToQuery[] = [
|
||||
{
|
||||
path: '',
|
||||
complete: false,
|
||||
Model,
|
||||
},
|
||||
];
|
||||
|
||||
pathSegments.every((segment, i) => {
|
||||
const lastIncompletePath = paths.find(({ complete }) => !complete);
|
||||
const { path } = lastIncompletePath;
|
||||
|
||||
const currentPath = path ? `${path}.${segment}` : segment;
|
||||
const currentSchemaType = schema.path(currentPath);
|
||||
const currentSchemaPathType = schema.pathType(currentPath);
|
||||
|
||||
if (currentSchemaPathType === 'nested') {
|
||||
lastIncompletePath.path = currentPath;
|
||||
return true;
|
||||
}
|
||||
|
||||
const upcomingSegment = pathSegments[i + 1];
|
||||
|
||||
if (currentSchemaType && currentSchemaPathType !== 'adhocOrUndefined') {
|
||||
const currentSchemaTypeOptions = getSchemaTypeOptions(currentSchemaType);
|
||||
|
||||
if (currentSchemaTypeOptions.localized) {
|
||||
const upcomingLocalizedPath = `${currentPath}.${upcomingSegment}`;
|
||||
const upcomingSchemaTypeWithLocale = schema.path(upcomingLocalizedPath);
|
||||
|
||||
if (upcomingSchemaTypeWithLocale) {
|
||||
lastIncompletePath.path = currentPath;
|
||||
return true;
|
||||
}
|
||||
|
||||
const localePath = `${currentPath}.${this.locale}`;
|
||||
const localizedSchemaType = schema.path(localePath);
|
||||
|
||||
if (localizedSchemaType || operator === 'near') {
|
||||
lastIncompletePath.path = localePath;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
lastIncompletePath.path = currentPath;
|
||||
return true;
|
||||
}
|
||||
|
||||
const priorSchemaType = schema.path(path);
|
||||
|
||||
if (priorSchemaType) {
|
||||
const priorSchemaTypeOptions = getSchemaTypeOptions(priorSchemaType);
|
||||
if (typeof priorSchemaTypeOptions.ref === 'string') {
|
||||
const RefModel = mongoose.model(priorSchemaTypeOptions.ref) as any;
|
||||
|
||||
lastIncompletePath.complete = true;
|
||||
|
||||
const remainingPath = pathSegments.slice(i).join('.');
|
||||
|
||||
paths = [
|
||||
...paths,
|
||||
...this.getLocalizedPaths(RefModel, remainingPath, operator),
|
||||
];
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (operator === 'near' || currentSchemaPathType === 'adhocOrUndefined') {
|
||||
lastIncompletePath.path = currentPath;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
// Convert the Payload key / value / operator into a MongoDB query
|
||||
async buildSearchParam(schema, incomingPath, val, operator): Promise<SearchParam> {
|
||||
async buildSearchParam({
|
||||
fields,
|
||||
incomingPath,
|
||||
val,
|
||||
operator,
|
||||
}: {
|
||||
fields: Field[],
|
||||
incomingPath: string,
|
||||
val: unknown,
|
||||
operator: string
|
||||
}): Promise<SearchParam> {
|
||||
// Replace GraphQL nested field double underscore formatting
|
||||
let sanitizedPath = incomingPath.replace(/__/gi, '.');
|
||||
if (sanitizedPath === 'id') sanitizedPath = '_id';
|
||||
|
||||
const collectionPaths = this.getLocalizedPaths(this.model, sanitizedPath, operator);
|
||||
const [{ path }] = collectionPaths;
|
||||
let paths: PathToQuery[] = [];
|
||||
|
||||
let hasCustomID = false;
|
||||
|
||||
if (sanitizedPath === '_id') {
|
||||
const customIDfield = this.req.payload.collections[this.collectionSlug]?.config.fields.find((field) => fieldAffectsData(field) && field.name === 'id');
|
||||
|
||||
let idFieldType: 'text' | 'number' = 'text';
|
||||
|
||||
if (customIDfield) {
|
||||
if (customIDfield?.type === 'text' || customIDfield?.type === 'number') {
|
||||
idFieldType = customIDfield.type;
|
||||
}
|
||||
|
||||
hasCustomID = true;
|
||||
}
|
||||
|
||||
paths.push({
|
||||
path: '_id',
|
||||
field: {
|
||||
name: 'id',
|
||||
type: idFieldType,
|
||||
},
|
||||
complete: true,
|
||||
collectionSlug: this.collectionSlug,
|
||||
});
|
||||
} else {
|
||||
paths = await this.getLocalizedPaths({
|
||||
collectionSlug: this.collectionSlug,
|
||||
globalSlug: this.globalSlug,
|
||||
fields,
|
||||
incomingPath: sanitizedPath,
|
||||
});
|
||||
}
|
||||
|
||||
const [{ path, field }] = paths;
|
||||
|
||||
if (path) {
|
||||
const schemaType = schema.path(path);
|
||||
const schemaOptions = getSchemaTypeOptions(schemaType);
|
||||
const formattedValue = sanitizeQueryValue(schemaType, path, operator, val);
|
||||
|
||||
if (!this.queryHiddenFields && (['salt', 'hash'].includes(path) || schemaType?.options?.hidden)) {
|
||||
return undefined;
|
||||
}
|
||||
const formattedValue = sanitizeQueryValue({
|
||||
ctx: this,
|
||||
field,
|
||||
path,
|
||||
operator,
|
||||
val,
|
||||
hasCustomID,
|
||||
});
|
||||
|
||||
// If there are multiple collections to search through,
|
||||
// Recursively build up a list of query constraints
|
||||
if (collectionPaths.length > 1) {
|
||||
if (paths.length > 1) {
|
||||
// Remove top collection and reverse array
|
||||
// to work backwards from top
|
||||
const collectionPathsToSearch = collectionPaths.slice(1).reverse();
|
||||
const pathsToQuery = paths.slice(1).reverse();
|
||||
|
||||
const initialRelationshipQuery = {
|
||||
value: {},
|
||||
} as SearchParam;
|
||||
|
||||
const relationshipQuery = await collectionPathsToSearch.reduce(async (priorQuery, { Model: SubModel, path: subPath }, i) => {
|
||||
const relationshipQuery = await pathsToQuery.reduce(async (priorQuery, { path: subPath, collectionSlug }, i) => {
|
||||
const priorQueryResult = await priorQuery;
|
||||
|
||||
const SubModel = this.req.payload.collections[collectionSlug].Model;
|
||||
|
||||
// On the "deepest" collection,
|
||||
// Search on the value passed through the query
|
||||
if (i === 0) {
|
||||
@@ -258,15 +268,17 @@ class ParamParser {
|
||||
[operator]: val,
|
||||
},
|
||||
},
|
||||
}, this.locale);
|
||||
req: this.req,
|
||||
overrideAccess: this.overrideAccess,
|
||||
});
|
||||
|
||||
const result = await SubModel.find(subQuery, subQueryOptions);
|
||||
|
||||
const $in = result.map((doc) => doc._id.toString());
|
||||
|
||||
if (collectionPathsToSearch.length === 1) return { path, value: { $in } };
|
||||
if (pathsToQuery.length === 1) return { path, value: { $in } };
|
||||
|
||||
const nextSubPath = collectionPathsToSearch[i + 1].path;
|
||||
const nextSubPath = pathsToQuery[i + 1].path;
|
||||
|
||||
return {
|
||||
value: { [nextSubPath]: { $in } },
|
||||
@@ -280,7 +292,7 @@ class ParamParser {
|
||||
|
||||
// If it is the last recursion
|
||||
// then pass through the search param
|
||||
if (i + 1 === collectionPathsToSearch.length) {
|
||||
if (i + 1 === pathsToQuery.length) {
|
||||
return { path, value: { $in } };
|
||||
}
|
||||
|
||||
@@ -297,51 +309,6 @@ class ParamParser {
|
||||
if (operator && validOperators.includes(operator)) {
|
||||
const operatorKey = operatorMap[operator];
|
||||
|
||||
let overrideQuery = false;
|
||||
let query;
|
||||
|
||||
// If there is a ref, this is a relationship or upload field
|
||||
// IDs can be either string, number, or ObjectID
|
||||
// So we need to build an `or` query for all these types
|
||||
if (schemaOptions && (schemaOptions.ref || schemaOptions.refPath)) {
|
||||
overrideQuery = true;
|
||||
|
||||
query = {
|
||||
$or: [
|
||||
{
|
||||
[path]: {
|
||||
[operatorKey]: formattedValue,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (typeof formattedValue === 'number' || (typeof formattedValue === 'string' && mongoose.Types.ObjectId.isValid(formattedValue))) {
|
||||
query.$or.push({
|
||||
[path]: {
|
||||
[operatorKey]: formattedValue.toString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof formattedValue === 'string') {
|
||||
if (!Number.isNaN(formattedValue)) {
|
||||
query.$or.push({
|
||||
[path]: {
|
||||
[operatorKey]: parseFloat(formattedValue),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If forced query
|
||||
if (overrideQuery) {
|
||||
return {
|
||||
value: query,
|
||||
};
|
||||
}
|
||||
|
||||
// Some operators like 'near' need to define a full query
|
||||
// so if there is no operator key, just return the value
|
||||
if (!operatorKey) {
|
||||
@@ -359,16 +326,254 @@ class ParamParser {
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Build up an array of auto-localized paths to search on
|
||||
// Multiple paths may be possible if searching on properties of relationship fields
|
||||
|
||||
async getLocalizedPaths({
|
||||
collectionSlug,
|
||||
globalSlug,
|
||||
fields,
|
||||
incomingPath,
|
||||
}: {
|
||||
collectionSlug?: string
|
||||
globalSlug?: string
|
||||
fields: Field[]
|
||||
incomingPath: string
|
||||
}): Promise<PathToQuery[]> {
|
||||
const pathSegments = incomingPath.split('.');
|
||||
|
||||
let paths: PathToQuery[] = [
|
||||
{
|
||||
path: '',
|
||||
complete: false,
|
||||
field: undefined,
|
||||
fields: flattenFields(fields, false),
|
||||
fieldPolicies: undefined,
|
||||
collectionSlug,
|
||||
},
|
||||
];
|
||||
|
||||
if (!this.overrideAccess) {
|
||||
if (collectionSlug) {
|
||||
const collection = { ...this.req.payload.collections[collectionSlug].config };
|
||||
collection.fields = fields;
|
||||
|
||||
if (!this.policies.collections[collectionSlug]) {
|
||||
const [policy, promises] = getEntityPolicies({
|
||||
req: this.req,
|
||||
entity: collection,
|
||||
operations: ['read'],
|
||||
type: 'collection',
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
this.policies.collections[collectionSlug] = policy;
|
||||
}
|
||||
|
||||
paths[0].fieldPolicies = this.policies.collections[collectionSlug].fields;
|
||||
|
||||
if (['salt', 'hash'].includes(incomingPath) && collection.auth && !collection.auth?.disableLocalStrategy) {
|
||||
this.errors.push({ path: incomingPath });
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
if (globalSlug) {
|
||||
if (!this.policies.globals[globalSlug]) {
|
||||
const global = { ...this.req.payload.globals.config.find(({ slug }) => slug === globalSlug) };
|
||||
global.fields = fields;
|
||||
|
||||
const [policy, promises] = getEntityPolicies({
|
||||
req: this.req,
|
||||
entity: global,
|
||||
operations: ['read'],
|
||||
type: 'global',
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
this.policies.globals[globalSlug] = policy;
|
||||
}
|
||||
|
||||
paths[0].fieldPolicies = this.policies.globals[globalSlug].fields;
|
||||
}
|
||||
}
|
||||
|
||||
// Use a 'some' so that we can bail out
|
||||
// if a relationship query is found
|
||||
// or if Rich Text / JSON
|
||||
|
||||
let done = false;
|
||||
|
||||
for (let i = 0; i < pathSegments.length; i += 1) {
|
||||
if (done) continue;
|
||||
|
||||
const segment = pathSegments[i];
|
||||
|
||||
const lastIncompletePath = paths.find(({ complete }) => !complete);
|
||||
|
||||
if (lastIncompletePath) {
|
||||
const { path } = lastIncompletePath;
|
||||
let currentPath = path ? `${path}.${segment}` : segment;
|
||||
|
||||
const matchedField = lastIncompletePath.fields.find((field) => fieldAffectsData(field) && field.name === segment);
|
||||
lastIncompletePath.field = matchedField;
|
||||
|
||||
if (currentPath === 'globalType' && this.globalSlug) {
|
||||
lastIncompletePath.path = currentPath;
|
||||
lastIncompletePath.complete = true;
|
||||
lastIncompletePath.field = {
|
||||
name: 'globalType',
|
||||
type: 'text',
|
||||
};
|
||||
|
||||
done = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (matchedField) {
|
||||
if (!this.overrideAccess) {
|
||||
const fieldAccess = lastIncompletePath.fieldPolicies[matchedField.name].read.permission;
|
||||
|
||||
if (!fieldAccess || ('hidden' in matchedField && matchedField.hidden)) {
|
||||
this.errors.push({ path: currentPath });
|
||||
done = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const nextSegment = pathSegments[i + 1];
|
||||
const nextSegmentIsLocale = this.localizationConfig && this.localizationConfig.locales.includes(nextSegment);
|
||||
|
||||
if (nextSegmentIsLocale) {
|
||||
// Skip the next iteration, because it's a locale
|
||||
i += 1;
|
||||
currentPath = `${currentPath}.${nextSegment}`;
|
||||
} else if ('localized' in matchedField && matchedField.localized) {
|
||||
currentPath = `${currentPath}.${this.req.locale}`;
|
||||
}
|
||||
|
||||
switch (matchedField.type) {
|
||||
case 'blocks':
|
||||
case 'richText':
|
||||
case 'json': {
|
||||
const upcomingSegments = pathSegments.slice(i + 1).join('.');
|
||||
lastIncompletePath.complete = true;
|
||||
lastIncompletePath.path = upcomingSegments ? `${currentPath}.${upcomingSegments}` : currentPath;
|
||||
done = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
case 'relationship':
|
||||
case 'upload': {
|
||||
// If this is a polymorphic relation,
|
||||
// We only support querying directly (no nested querying)
|
||||
if (typeof matchedField.relationTo !== 'string') {
|
||||
const lastSegmentIsValid = ['value', 'relationTo'].includes(pathSegments[pathSegments.length - 1]);
|
||||
|
||||
if (lastSegmentIsValid) {
|
||||
lastIncompletePath.complete = true;
|
||||
lastIncompletePath.path = pathSegments.join('.');
|
||||
} else {
|
||||
this.errors.push({ path: currentPath });
|
||||
done = true;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
lastIncompletePath.complete = true;
|
||||
lastIncompletePath.collectionSlug = matchedField.relationTo;
|
||||
lastIncompletePath.path = currentPath;
|
||||
|
||||
const nestedPathToQuery = pathSegments.slice(nextSegmentIsLocale ? i + 2 : i + 1).join('.');
|
||||
|
||||
if (nestedPathToQuery) {
|
||||
const relatedCollection = this.req.payload.collections[matchedField.relationTo as string].config;
|
||||
|
||||
const remainingPaths = await this.getLocalizedPaths({
|
||||
collectionSlug: relatedCollection.slug,
|
||||
fields: relatedCollection.fields,
|
||||
incomingPath: nestedPathToQuery,
|
||||
});
|
||||
|
||||
paths = [
|
||||
...paths,
|
||||
...remainingPaths,
|
||||
];
|
||||
}
|
||||
|
||||
done = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
if ('fields' in lastIncompletePath.field) {
|
||||
lastIncompletePath.fields = flattenFields(lastIncompletePath.field.fields, false);
|
||||
}
|
||||
|
||||
if (!this.overrideAccess && 'fields' in lastIncompletePath.fieldPolicies[lastIncompletePath.field.name]) {
|
||||
lastIncompletePath.fieldPolicies = lastIncompletePath.fieldPolicies[lastIncompletePath.field.name].fields;
|
||||
}
|
||||
|
||||
if (i + 1 === pathSegments.length) lastIncompletePath.complete = true;
|
||||
lastIncompletePath.path = currentPath;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.errors.push({ path: currentPath });
|
||||
done = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
}
|
||||
|
||||
type GetBuildQueryPluginArgs = {
|
||||
collectionSlug?: string
|
||||
versionsFields?: Field[]
|
||||
}
|
||||
|
||||
export type BuildQueryArgs = {
|
||||
req: PayloadRequest
|
||||
where: Where
|
||||
overrideAccess: boolean
|
||||
globalSlug?: string
|
||||
}
|
||||
|
||||
// This plugin asynchronously builds a list of Mongoose query constraints
|
||||
// which can then be used in subsequent Mongoose queries.
|
||||
function buildQueryPlugin(schema) {
|
||||
const modifiedSchema = schema;
|
||||
async function buildQuery(rawParams, locale, queryHiddenFields = false) {
|
||||
const paramParser = new ParamParser(this, rawParams, locale, queryHiddenFields);
|
||||
const params = await paramParser.parse();
|
||||
return params.searchParams;
|
||||
}
|
||||
modifiedSchema.statics.buildQuery = buildQuery;
|
||||
}
|
||||
export default buildQueryPlugin;
|
||||
const getBuildQueryPlugin = ({
|
||||
collectionSlug,
|
||||
versionsFields,
|
||||
}: GetBuildQueryPluginArgs = {}) => {
|
||||
return function buildQueryPlugin(schema) {
|
||||
const modifiedSchema = schema;
|
||||
async function buildQuery({ req, where, overrideAccess = false, globalSlug }: BuildQueryArgs): Promise<Record<string, unknown>> {
|
||||
const paramParser = new ParamParser({
|
||||
req,
|
||||
collectionSlug,
|
||||
globalSlug,
|
||||
versionsFields,
|
||||
model: this,
|
||||
where,
|
||||
overrideAccess,
|
||||
});
|
||||
const result = await paramParser.parse();
|
||||
|
||||
if (paramParser.errors.length > 0) {
|
||||
throw new QueryError(paramParser.errors);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
modifiedSchema.statics.buildQuery = buildQuery;
|
||||
};
|
||||
};
|
||||
|
||||
export default getBuildQueryPlugin;
|
||||
|
||||
@@ -1,27 +1,40 @@
|
||||
import mongoose, { SchemaType } from 'mongoose';
|
||||
import mongoose from 'mongoose';
|
||||
import { createArrayFromCommaDelineated } from './createArrayFromCommaDelineated';
|
||||
import { getSchemaTypeOptions } from './getSchemaTypeOptions';
|
||||
import wordBoundariesRegex from '../utilities/wordBoundariesRegex';
|
||||
import { Field, TabAsField } from '../fields/config/types';
|
||||
import { ParamParser } from './buildQuery';
|
||||
|
||||
export const sanitizeQueryValue = (schemaType: SchemaType, path: string, operator: string, val: any): unknown => {
|
||||
type SanitizeQueryValueArgs = {
|
||||
ctx: ParamParser,
|
||||
field: Field | TabAsField,
|
||||
path: string,
|
||||
operator: string,
|
||||
val: any
|
||||
hasCustomID: boolean
|
||||
}
|
||||
|
||||
export const sanitizeQueryValue = ({ ctx, field, path, operator, val, hasCustomID }: SanitizeQueryValueArgs): unknown => {
|
||||
let formattedValue = val;
|
||||
const schemaOptions = getSchemaTypeOptions(schemaType);
|
||||
|
||||
// Disregard invalid _ids
|
||||
|
||||
if (path === '_id' && typeof val === 'string' && val.split(',').length === 1) {
|
||||
if (schemaType?.instance === 'ObjectID') {
|
||||
if (!hasCustomID) {
|
||||
const isValid = mongoose.Types.ObjectId.isValid(val);
|
||||
|
||||
formattedValue = new mongoose.Types.ObjectId(val);
|
||||
|
||||
if (!isValid) {
|
||||
ctx.errors.push({ path });
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (schemaType?.instance === 'Number') {
|
||||
if (field.type === 'number') {
|
||||
const parsedNumber = parseFloat(val);
|
||||
|
||||
if (Number.isNaN(parsedNumber)) {
|
||||
ctx.errors.push({ path });
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -29,17 +42,34 @@ export const sanitizeQueryValue = (schemaType: SchemaType, path: string, operato
|
||||
|
||||
// Cast incoming values as proper searchable types
|
||||
|
||||
if (schemaType?.instance === 'Boolean' && typeof val === 'string') {
|
||||
if (field.type === 'checkbox' && typeof val === 'string') {
|
||||
if (val.toLowerCase() === 'true') formattedValue = true;
|
||||
if (val.toLowerCase() === 'false') formattedValue = false;
|
||||
}
|
||||
|
||||
if (schemaType?.instance === 'Number' && typeof val === 'string') {
|
||||
if (field.type === 'number' && typeof val === 'string') {
|
||||
formattedValue = Number(val);
|
||||
}
|
||||
|
||||
if ((schemaOptions?.ref || schemaOptions?.refPath) && val === 'null') {
|
||||
formattedValue = null;
|
||||
if (['relationship', 'upload'].includes(field.type)) {
|
||||
if (val === 'null') {
|
||||
formattedValue = null;
|
||||
}
|
||||
|
||||
if (operator === 'in' && Array.isArray(formattedValue)) {
|
||||
formattedValue = formattedValue.reduce((formattedValues, inVal) => {
|
||||
const newValues = [inVal];
|
||||
if (mongoose.Types.ObjectId.isValid(inVal)) newValues.push(new mongoose.Types.ObjectId(inVal));
|
||||
|
||||
const parsedNumber = parseFloat(inVal);
|
||||
if (!Number.isNaN(parsedNumber)) newValues.push(parsedNumber);
|
||||
|
||||
return [
|
||||
...formattedValues,
|
||||
...newValues,
|
||||
];
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
|
||||
// Set up specific formatting necessary by operators
|
||||
@@ -74,23 +104,6 @@ export const sanitizeQueryValue = (schemaType: SchemaType, path: string, operato
|
||||
formattedValue = createArrayFromCommaDelineated(formattedValue);
|
||||
}
|
||||
|
||||
if (schemaOptions && (schemaOptions.ref || schemaOptions.refPath) && operator === 'in') {
|
||||
if (Array.isArray(formattedValue)) {
|
||||
formattedValue = formattedValue.reduce((formattedValues, inVal) => {
|
||||
const newValues = [inVal];
|
||||
if (mongoose.Types.ObjectId.isValid(inVal)) newValues.push(new mongoose.Types.ObjectId(inVal));
|
||||
|
||||
const parsedNumber = parseFloat(inVal);
|
||||
if (!Number.isNaN(parsedNumber)) newValues.push(parsedNumber);
|
||||
|
||||
return [
|
||||
...formattedValues,
|
||||
...newValues,
|
||||
];
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
|
||||
if (path !== '_id') {
|
||||
if (operator === 'contains') {
|
||||
formattedValue = { $regex: formattedValue, $options: 'i' };
|
||||
@@ -1,18 +1,18 @@
|
||||
import { WhereField } from '../types';
|
||||
import { WhereField, Where } from '../types';
|
||||
|
||||
const flattenWhereConstraints = (query): WhereField[] => {
|
||||
if (!query.where && !query.and && !query.or) {
|
||||
return Object.keys(query).map((key) => query[key]);
|
||||
// Take a where query and flatten it to all top-level operators
|
||||
const flattenWhereConstraints = (query: Where): WhereField[] => Object.entries(query).reduce((flattenedConstraints, [key, val]) => {
|
||||
if ((key === 'and' || key === 'or') && Array.isArray(val)) {
|
||||
return [
|
||||
...flattenedConstraints,
|
||||
...val.map((subVal) => flattenWhereConstraints(subVal)),
|
||||
];
|
||||
}
|
||||
if (query.where) {
|
||||
const whereResult = flattenWhereConstraints(query.where);
|
||||
return Object.keys(whereResult).map((key) => whereResult[key]);
|
||||
}
|
||||
const nested = [...query.or || [], ...query.and || []];
|
||||
if (nested.length > 0) {
|
||||
return nested.flatMap((nest) => flattenWhereConstraints(nest));
|
||||
}
|
||||
return query;
|
||||
};
|
||||
|
||||
return [
|
||||
...flattenedConstraints,
|
||||
val,
|
||||
];
|
||||
}, []);
|
||||
|
||||
export default flattenWhereConstraints;
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
import { Access } from '../config/types';
|
||||
import { AllOperations, Where, Document } from '../types';
|
||||
import { AllOperations, Document, Where } from '../types';
|
||||
import { FieldAccess, tabHasName } from '../fields/config/types';
|
||||
import type { CollectionConfig } from '../collections/config/types';
|
||||
import type { GlobalConfig } from '../globals/config/types';
|
||||
import type { SanitizedCollectionConfig } from '../collections/config/types';
|
||||
import { TypeWithID } from '../collections/config/types';
|
||||
import type { SanitizedGlobalConfig } from '../globals/config/types';
|
||||
import type { PayloadRequest } from '../express/types';
|
||||
import type { CollectionPermission, GlobalPermission } from '../auth/types';
|
||||
import { TypeWithID } from '../collections/config/types';
|
||||
|
||||
type Args = ({
|
||||
type Args = {
|
||||
req: PayloadRequest
|
||||
operations: AllOperations[]
|
||||
id?: string
|
||||
} & ({
|
||||
type: 'collection'
|
||||
entity: CollectionConfig
|
||||
} | {
|
||||
type: 'global'
|
||||
entity: GlobalConfig
|
||||
}))
|
||||
type: 'collection' | 'global'
|
||||
entity: SanitizedCollectionConfig | SanitizedGlobalConfig
|
||||
}
|
||||
|
||||
type ReturnType<T extends Args> = T['type'] extends 'global' ? [GlobalPermission, Promise<void>[]] : [CollectionPermission, Promise<void>[]]
|
||||
|
||||
type CreateAccessPromise = (args: {
|
||||
@@ -111,14 +108,15 @@ export function getEntityPolicies<T extends Args>(args: T): ReturnType<T> {
|
||||
}
|
||||
};
|
||||
|
||||
const executeFieldPolicies = ({
|
||||
policiesObj = {},
|
||||
const executeFieldPolicies = async ({
|
||||
policiesObj,
|
||||
fields,
|
||||
operation,
|
||||
entityAccessPromise,
|
||||
}) => {
|
||||
const mutablePolicies = policiesObj;
|
||||
const mutablePolicies = policiesObj.fields;
|
||||
|
||||
fields.forEach((field) => {
|
||||
fields.forEach(async (field) => {
|
||||
if (field.name) {
|
||||
if (!mutablePolicies[field.name]) mutablePolicies[field.name] = {};
|
||||
|
||||
@@ -131,39 +129,44 @@ export function getEntityPolicies<T extends Args>(args: T): ReturnType<T> {
|
||||
accessLevel: 'field',
|
||||
}));
|
||||
} else {
|
||||
if (entityAccessPromise) await entityAccessPromise;
|
||||
mutablePolicies[field.name][operation] = {
|
||||
permission: isLoggedIn,
|
||||
permission: policiesObj[operation]?.permission,
|
||||
};
|
||||
}
|
||||
|
||||
if (field.fields) {
|
||||
if (!mutablePolicies[field.name].fields) mutablePolicies[field.name].fields = {};
|
||||
executeFieldPolicies({
|
||||
policiesObj: mutablePolicies[field.name].fields,
|
||||
policiesObj: mutablePolicies[field.name],
|
||||
fields: field.fields,
|
||||
operation,
|
||||
entityAccessPromise,
|
||||
});
|
||||
}
|
||||
} else if (field.fields) {
|
||||
executeFieldPolicies({
|
||||
policiesObj: mutablePolicies,
|
||||
policiesObj,
|
||||
fields: field.fields,
|
||||
operation,
|
||||
entityAccessPromise,
|
||||
});
|
||||
} else if (field.type === 'tabs') {
|
||||
field.tabs.forEach((tab) => {
|
||||
if (tabHasName(tab)) {
|
||||
if (!mutablePolicies[tab.name]) mutablePolicies[tab.name] = { fields: {} };
|
||||
executeFieldPolicies({
|
||||
policiesObj: mutablePolicies[tab.name].fields,
|
||||
policiesObj: mutablePolicies[tab.name],
|
||||
fields: tab.fields,
|
||||
operation,
|
||||
entityAccessPromise,
|
||||
});
|
||||
} else {
|
||||
executeFieldPolicies({
|
||||
policiesObj: mutablePolicies,
|
||||
policiesObj,
|
||||
fields: tab.fields,
|
||||
operation,
|
||||
entityAccessPromise,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -172,24 +175,28 @@ export function getEntityPolicies<T extends Args>(args: T): ReturnType<T> {
|
||||
};
|
||||
|
||||
operations.forEach((operation) => {
|
||||
executeFieldPolicies({
|
||||
policiesObj: policies.fields,
|
||||
fields: entity.fields,
|
||||
operation,
|
||||
});
|
||||
let entityAccessPromise: Promise<void>;
|
||||
|
||||
if (typeof entity.access[operation] === 'function') {
|
||||
promises.push(createAccessPromise({
|
||||
entityAccessPromise = createAccessPromise({
|
||||
policiesObj: policies,
|
||||
access: entity.access[operation],
|
||||
operation,
|
||||
accessLevel: 'entity',
|
||||
}));
|
||||
});
|
||||
promises.push(entityAccessPromise);
|
||||
} else {
|
||||
policies[operation] = {
|
||||
permission: isLoggedIn,
|
||||
};
|
||||
}
|
||||
|
||||
executeFieldPolicies({
|
||||
policiesObj: policies,
|
||||
fields: entity.fields,
|
||||
operation,
|
||||
entityAccessPromise,
|
||||
});
|
||||
});
|
||||
|
||||
return [policies, promises] as ReturnType<T>;
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { IncomingCollectionVersions, IncomingGlobalVersions } from './types';
|
||||
|
||||
export const versionCollectionDefaults: IncomingCollectionVersions = {
|
||||
drafts: {
|
||||
autosave: {
|
||||
interval: 2000, // in milliseconds
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const versionGlobalDefaults: IncomingGlobalVersions = {
|
||||
drafts: {
|
||||
autosave: {
|
||||
interval: 2000, // in milliseconds
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PaginateOptions } from 'mongoose';
|
||||
import { AccessResult } from '../../config/types';
|
||||
import { Where } from '../../types';
|
||||
import { PayloadRequest, Where } from '../../types';
|
||||
import { Payload } from '../../payload';
|
||||
import { PaginatedDocs } from '../../mongoose/types';
|
||||
import { Collection, CollectionModel, TypeWithID } from '../../collections/config/types';
|
||||
@@ -17,7 +17,8 @@ type AggregateVersion<T> = {
|
||||
type Args = {
|
||||
accessResult: AccessResult
|
||||
collection: Collection
|
||||
locale: string
|
||||
req: PayloadRequest
|
||||
overrideAccess: boolean
|
||||
paginationOptions?: PaginateOptions
|
||||
payload: Payload
|
||||
where: Where
|
||||
@@ -26,7 +27,8 @@ type Args = {
|
||||
export const queryDrafts = async <T extends TypeWithID>({
|
||||
accessResult,
|
||||
collection,
|
||||
locale,
|
||||
req,
|
||||
overrideAccess,
|
||||
payload,
|
||||
paginationOptions,
|
||||
where: incomingWhere,
|
||||
@@ -35,21 +37,23 @@ export const queryDrafts = async <T extends TypeWithID>({
|
||||
|
||||
const where = appendVersionToQueryKey(incomingWhere || {});
|
||||
|
||||
const versionQueryToBuild: { where: Where } = {
|
||||
where: {
|
||||
...where,
|
||||
and: [
|
||||
...where?.and || [],
|
||||
],
|
||||
},
|
||||
const versionQueryToBuild: Where = {
|
||||
...where,
|
||||
and: [
|
||||
...where?.and || [],
|
||||
],
|
||||
};
|
||||
|
||||
if (hasWhereAccessResult(accessResult)) {
|
||||
const versionAccessResult = appendVersionToQueryKey(accessResult);
|
||||
versionQueryToBuild.where.and.push(versionAccessResult);
|
||||
versionQueryToBuild.and.push(versionAccessResult);
|
||||
}
|
||||
|
||||
const versionQuery = await VersionModel.buildQuery(versionQueryToBuild, locale);
|
||||
const versionQuery = await VersionModel.buildQuery({
|
||||
where: versionQueryToBuild,
|
||||
req,
|
||||
overrideAccess,
|
||||
});
|
||||
|
||||
const aggregate = VersionModel.aggregate<AggregateVersion<T>>([
|
||||
// Sort so that newest are first
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Payload } from '../../payload';
|
||||
import { docHasTimestamps, Where } from '../../types';
|
||||
import { docHasTimestamps, PayloadRequest, Where } from '../../types';
|
||||
import { hasWhereAccessResult } from '../../auth';
|
||||
import { AccessResult } from '../../config/types';
|
||||
import { CollectionModel, SanitizedCollectionConfig, TypeWithID } from '../../collections/config/types';
|
||||
@@ -12,7 +12,8 @@ type Arguments<T> = {
|
||||
entity: SanitizedCollectionConfig | SanitizedGlobalConfig
|
||||
entityType: 'collection' | 'global'
|
||||
doc: T
|
||||
locale: string
|
||||
req: PayloadRequest
|
||||
overrideAccess: boolean
|
||||
accessResult: AccessResult
|
||||
}
|
||||
|
||||
@@ -21,25 +22,24 @@ const replaceWithDraftIfAvailable = async <T extends TypeWithID>({
|
||||
entity,
|
||||
entityType,
|
||||
doc,
|
||||
locale,
|
||||
req,
|
||||
overrideAccess,
|
||||
accessResult,
|
||||
}: Arguments<T>): Promise<T> => {
|
||||
const VersionModel = payload.versions[entity.slug] as CollectionModel;
|
||||
|
||||
const queryToBuild: { where: Where } = {
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
'version._status': {
|
||||
equals: 'draft',
|
||||
},
|
||||
const queryToBuild: Where = {
|
||||
and: [
|
||||
{
|
||||
'version._status': {
|
||||
equals: 'draft',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (entityType === 'collection') {
|
||||
queryToBuild.where.and.push({
|
||||
queryToBuild.and.push({
|
||||
parent: {
|
||||
equals: doc.id,
|
||||
},
|
||||
@@ -47,7 +47,7 @@ const replaceWithDraftIfAvailable = async <T extends TypeWithID>({
|
||||
}
|
||||
|
||||
if (docHasTimestamps(doc)) {
|
||||
queryToBuild.where.and.push({
|
||||
queryToBuild.and.push({
|
||||
updatedAt: {
|
||||
greater_than: doc.updatedAt,
|
||||
},
|
||||
@@ -56,10 +56,15 @@ const replaceWithDraftIfAvailable = async <T extends TypeWithID>({
|
||||
|
||||
if (hasWhereAccessResult(accessResult)) {
|
||||
const versionAccessResult = appendVersionToQueryKey(accessResult);
|
||||
queryToBuild.where.and.push(versionAccessResult);
|
||||
queryToBuild.and.push(versionAccessResult);
|
||||
}
|
||||
|
||||
const query = await VersionModel.buildQuery(queryToBuild, locale);
|
||||
const query = await VersionModel.buildQuery({
|
||||
where: queryToBuild,
|
||||
req,
|
||||
overrideAccess,
|
||||
globalSlug: entityType === 'global' ? entity.slug : undefined,
|
||||
});
|
||||
|
||||
let draft = await VersionModel.findOne(query, {}, {
|
||||
lean: true,
|
||||
|
||||
Reference in New Issue
Block a user