feat: refactors buildQuery to rely on fields instead of mongoose
This commit is contained in:
@@ -78,7 +78,7 @@ export default function initCollectionsLocal(ctx: Payload): void {
|
|||||||
);
|
);
|
||||||
|
|
||||||
versionSchema.plugin(paginate, { useEstimatedCount: true })
|
versionSchema.plugin(paginate, { useEstimatedCount: true })
|
||||||
.plugin(getBuildQueryPlugin({ collectionSlug: collection.slug, isVersionsModel: true }));
|
.plugin(getBuildQueryPlugin({ collectionSlug: collection.slug, versionsFields: versionCollectionFields }));
|
||||||
|
|
||||||
if (collection.versions?.drafts) {
|
if (collection.versions?.drafts) {
|
||||||
versionSchema.plugin(mongooseAggregatePaginate);
|
versionSchema.plugin(mongooseAggregatePaginate);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const buildModel = (config: SanitizedConfig): GlobalModel | null => {
|
|||||||
if (config.globals && config.globals.length > 0) {
|
if (config.globals && config.globals.length > 0) {
|
||||||
const globalsSchema = new mongoose.Schema({}, { discriminatorKey: 'globalType', timestamps: true, minimize: false });
|
const globalsSchema = new mongoose.Schema({}, { discriminatorKey: 'globalType', timestamps: true, minimize: false });
|
||||||
|
|
||||||
globalsSchema.plugin(getBuildQueryPlugin({ isGlobalModel: true }));
|
globalsSchema.plugin(getBuildQueryPlugin());
|
||||||
|
|
||||||
const Globals = mongoose.model('globals', globalsSchema) as unknown as GlobalModel;
|
const Globals = mongoose.model('globals', globalsSchema) as unknown as GlobalModel;
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export default function initGlobalsLocal(ctx: Payload): void {
|
|||||||
);
|
);
|
||||||
|
|
||||||
versionSchema.plugin(paginate, { useEstimatedCount: true })
|
versionSchema.plugin(paginate, { useEstimatedCount: true })
|
||||||
.plugin(getBuildQueryPlugin({ globalSlug: global.slug, isVersionsModel: true }));
|
.plugin(getBuildQueryPlugin({ versionsFields: versionGlobalFields }));
|
||||||
|
|
||||||
ctx.versions[global.slug] = mongoose.model(versionModelName, versionSchema) as CollectionModel;
|
ctx.versions[global.slug] = mongoose.model(versionModelName, versionSchema) as CollectionModel;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ async function findOne<T extends Record<string, unknown>>(args: Args): Promise<T
|
|||||||
where: queryToBuild,
|
where: queryToBuild,
|
||||||
req,
|
req,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
globalSlug: slug,
|
||||||
});
|
});
|
||||||
|
|
||||||
// /////////////////////////////////////
|
// /////////////////////////////////////
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ async function findVersionByID<T extends TypeWithVersion<T> = any>(args: Argumen
|
|||||||
req: {
|
req: {
|
||||||
t,
|
t,
|
||||||
payload,
|
payload,
|
||||||
locale,
|
|
||||||
},
|
},
|
||||||
disableErrors,
|
disableErrors,
|
||||||
currentDepth,
|
currentDepth,
|
||||||
@@ -68,6 +67,7 @@ async function findVersionByID<T extends TypeWithVersion<T> = any>(args: Argumen
|
|||||||
where: queryToBuild,
|
where: queryToBuild,
|
||||||
req,
|
req,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
globalSlug: globalConfig.slug,
|
||||||
});
|
});
|
||||||
|
|
||||||
// /////////////////////////////////////
|
// /////////////////////////////////////
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ async function findVersions<T extends TypeWithVersion<T>>(
|
|||||||
where: queryToBuild,
|
where: queryToBuild,
|
||||||
req,
|
req,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
globalSlug: globalConfig.slug,
|
||||||
});
|
});
|
||||||
|
|
||||||
// /////////////////////////////////////
|
// /////////////////////////////////////
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ async function update<TSlug extends keyof GeneratedTypes['globals']>(
|
|||||||
slug,
|
slug,
|
||||||
req,
|
req,
|
||||||
req: {
|
req: {
|
||||||
locale,
|
|
||||||
payload,
|
payload,
|
||||||
payload: {
|
payload: {
|
||||||
globals: {
|
globals: {
|
||||||
@@ -80,6 +79,7 @@ async function update<TSlug extends keyof GeneratedTypes['globals']>(
|
|||||||
where: queryToBuild,
|
where: queryToBuild,
|
||||||
req,
|
req,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
globalSlug: slug,
|
||||||
});
|
});
|
||||||
|
|
||||||
// /////////////////////////////////////
|
// /////////////////////////////////////
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
|
/* eslint-disable no-continue */
|
||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
/* eslint-disable no-restricted-syntax */
|
/* eslint-disable no-restricted-syntax */
|
||||||
import deepmerge from 'deepmerge';
|
import deepmerge from 'deepmerge';
|
||||||
import mongoose, { FilterQuery } from 'mongoose';
|
import { FilterQuery } from 'mongoose';
|
||||||
import { combineMerge } from '../utilities/combineMerge';
|
import { combineMerge } from '../utilities/combineMerge';
|
||||||
import { CollectionModel } from '../collections/config/types';
|
|
||||||
import { getSchemaTypeOptions } from './getSchemaTypeOptions';
|
|
||||||
import { operatorMap } from './operatorMap';
|
import { operatorMap } from './operatorMap';
|
||||||
import { sanitizeQueryValue } from './sanitizeFormattedValue';
|
import { sanitizeQueryValue } from './sanitizeQueryValue';
|
||||||
import { PayloadRequest, Where } from '../types';
|
import { PayloadRequest, Where } from '../types';
|
||||||
|
import { Field, FieldAffectingData, TabAsField, UIField, fieldAffectsData } 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';
|
||||||
|
|
||||||
const validOperators = ['like', 'contains', 'in', 'all', 'not_in', 'greater_than_equal', 'greater_than', 'less_than_equal', 'less_than', 'not_equals', 'equals', 'exists', 'near'];
|
const validOperators = ['like', 'contains', 'in', 'all', 'not_in', 'greater_than_equal', 'greater_than', 'less_than_equal', 'less_than', 'not_equals', 'equals', 'exists', 'near'];
|
||||||
|
|
||||||
@@ -16,18 +20,15 @@ const subQueryOptions = {
|
|||||||
lean: true,
|
lean: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
type ParseType = {
|
|
||||||
searchParams?:
|
|
||||||
{
|
|
||||||
[key: string]: any;
|
|
||||||
};
|
|
||||||
sort?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type PathToQuery = {
|
type PathToQuery = {
|
||||||
complete: boolean
|
complete: boolean
|
||||||
|
collectionSlug?: string
|
||||||
path: string
|
path: string
|
||||||
Model: CollectionModel
|
field: Field | TabAsField
|
||||||
|
fields?: (FieldAffectingData | UIField | TabAsField)[]
|
||||||
|
fieldPolicies?: {
|
||||||
|
[field: string]: FieldPermissions
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchParam = {
|
type SearchParam = {
|
||||||
@@ -35,15 +36,23 @@ type SearchParam = {
|
|||||||
value: unknown,
|
value: unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
class ParamParser {
|
type ParamParserArgs = {
|
||||||
|
req: PayloadRequest
|
||||||
|
collectionSlug?: string
|
||||||
|
globalSlug?: string
|
||||||
|
versionsFields?: Field[]
|
||||||
|
model: any
|
||||||
|
where: Where
|
||||||
|
overrideAccess?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryError = { path: string }
|
||||||
|
|
||||||
|
export class ParamParser {
|
||||||
collectionSlug?: string
|
collectionSlug?: string
|
||||||
|
|
||||||
globalSlug?: string
|
globalSlug?: string
|
||||||
|
|
||||||
isGlobalModel?: boolean
|
|
||||||
|
|
||||||
isVersionsModel?: boolean
|
|
||||||
|
|
||||||
overrideAccess: boolean
|
overrideAccess: boolean
|
||||||
|
|
||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
@@ -52,49 +61,68 @@ class ParamParser {
|
|||||||
|
|
||||||
model: any;
|
model: any;
|
||||||
|
|
||||||
query: {
|
fields: Field[]
|
||||||
searchParams: {
|
|
||||||
[key: string]: any;
|
localizationConfig: SanitizedConfig['localization']
|
||||||
|
|
||||||
|
policies: {
|
||||||
|
collections?: {
|
||||||
|
[collectionSlug: string]: CollectionPermission;
|
||||||
};
|
};
|
||||||
sort: boolean;
|
globals?: {
|
||||||
};
|
[globalSlug: string]: GlobalPermission;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
errors: QueryError[]
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
req,
|
req,
|
||||||
collectionSlug,
|
collectionSlug,
|
||||||
globalSlug,
|
globalSlug,
|
||||||
isGlobalModel,
|
versionsFields,
|
||||||
isVersionsModel,
|
|
||||||
model,
|
model,
|
||||||
where,
|
where,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
}) {
|
}: ParamParserArgs) {
|
||||||
this.req = req;
|
this.req = req;
|
||||||
this.collectionSlug = collectionSlug;
|
this.collectionSlug = collectionSlug;
|
||||||
this.globalSlug = globalSlug;
|
this.globalSlug = globalSlug;
|
||||||
this.isGlobalModel = isGlobalModel;
|
|
||||||
this.isVersionsModel = isVersionsModel;
|
|
||||||
this.parse = this.parse.bind(this);
|
this.parse = this.parse.bind(this);
|
||||||
this.model = model;
|
this.model = model;
|
||||||
this.where = where;
|
this.where = where;
|
||||||
this.overrideAccess = overrideAccess;
|
this.overrideAccess = overrideAccess;
|
||||||
this.query = {
|
this.localizationConfig = req.payload.config.localization;
|
||||||
searchParams: {},
|
this.policies = {
|
||||||
sort: false,
|
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
|
// Entry point to the ParamParser class
|
||||||
|
|
||||||
async parse(): Promise<ParseType> {
|
async parse(): Promise<Record<string, unknown>> {
|
||||||
if (typeof this.where === 'object') {
|
if (typeof this.where === 'object') {
|
||||||
this.query.searchParams = await this.parsePathOrRelation(this.where);
|
const query = await this.parsePathOrRelation(this.where);
|
||||||
return this.query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
async parsePathOrRelation(object) {
|
async parsePathOrRelation(object: Where): Promise<Record<string, unknown>> {
|
||||||
let result = {} as FilterQuery<any>;
|
let result = {} as FilterQuery<any>;
|
||||||
// We need to determine if the whereKey is an AND, OR, or a schema path
|
// We need to determine if the whereKey is an AND, OR, or a schema path
|
||||||
for (const relationOrPath of Object.keys(object)) {
|
for (const relationOrPath of Object.keys(object)) {
|
||||||
@@ -114,7 +142,12 @@ class ParamParser {
|
|||||||
if (typeof pathOperators === 'object') {
|
if (typeof pathOperators === 'object') {
|
||||||
for (const operator of Object.keys(pathOperators)) {
|
for (const operator of Object.keys(pathOperators)) {
|
||||||
if (validOperators.includes(operator)) {
|
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) {
|
if (searchParam?.value && searchParam?.path) {
|
||||||
result = {
|
result = {
|
||||||
@@ -148,123 +181,85 @@ class ParamParser {
|
|||||||
return completedConditions;
|
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.req.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
|
// 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
|
// Replace GraphQL nested field double underscore formatting
|
||||||
let sanitizedPath = incomingPath.replace(/__/gi, '.');
|
let sanitizedPath = incomingPath.replace(/__/gi, '.');
|
||||||
if (sanitizedPath === 'id') sanitizedPath = '_id';
|
if (sanitizedPath === 'id') sanitizedPath = '_id';
|
||||||
|
|
||||||
const collectionPaths = this.getLocalizedPaths(this.model, sanitizedPath, operator);
|
let paths: PathToQuery[] = [];
|
||||||
const [{ path }] = collectionPaths;
|
|
||||||
|
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) {
|
if (path) {
|
||||||
const schemaType = schema.path(path);
|
const formattedValue = sanitizeQueryValue({
|
||||||
const schemaOptions = getSchemaTypeOptions(schemaType);
|
ctx: this,
|
||||||
const formattedValue = sanitizeQueryValue(schemaType, path, operator, val);
|
field,
|
||||||
|
path,
|
||||||
if (!this.overrideAccess && (['salt', 'hash'].includes(path) || schemaType?.options?.hidden)) {
|
operator,
|
||||||
return undefined;
|
val,
|
||||||
}
|
hasCustomID,
|
||||||
|
});
|
||||||
|
|
||||||
// If there are multiple collections to search through,
|
// If there are multiple collections to search through,
|
||||||
// Recursively build up a list of query constraints
|
// Recursively build up a list of query constraints
|
||||||
if (collectionPaths.length > 1) {
|
if (paths.length > 1) {
|
||||||
// Remove top collection and reverse array
|
// Remove top collection and reverse array
|
||||||
// to work backwards from top
|
// to work backwards from top
|
||||||
const collectionPathsToSearch = collectionPaths.slice(1).reverse();
|
const pathsToQuery = paths.slice(1).reverse();
|
||||||
|
|
||||||
const initialRelationshipQuery = {
|
const initialRelationshipQuery = {
|
||||||
value: {},
|
value: {},
|
||||||
} as SearchParam;
|
} 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 priorQueryResult = await priorQuery;
|
||||||
|
|
||||||
|
const SubModel = this.req.payload.collections[collectionSlug].Model;
|
||||||
|
|
||||||
// On the "deepest" collection,
|
// On the "deepest" collection,
|
||||||
// Search on the value passed through the query
|
// Search on the value passed through the query
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
@@ -282,9 +277,9 @@ class ParamParser {
|
|||||||
|
|
||||||
const $in = result.map((doc) => doc._id.toString());
|
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 {
|
return {
|
||||||
value: { [nextSubPath]: { $in } },
|
value: { [nextSubPath]: { $in } },
|
||||||
@@ -298,7 +293,7 @@ class ParamParser {
|
|||||||
|
|
||||||
// If it is the last recursion
|
// If it is the last recursion
|
||||||
// then pass through the search param
|
// then pass through the search param
|
||||||
if (i + 1 === collectionPathsToSearch.length) {
|
if (i + 1 === pathsToQuery.length) {
|
||||||
return { path, value: { $in } };
|
return { path, value: { $in } };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,51 +310,6 @@ class ParamParser {
|
|||||||
if (operator && validOperators.includes(operator)) {
|
if (operator && validOperators.includes(operator)) {
|
||||||
const operatorKey = operatorMap[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
|
// Some operators like 'near' need to define a full query
|
||||||
// so if there is no operator key, just return the value
|
// so if there is no operator key, just return the value
|
||||||
if (!operatorKey) {
|
if (!operatorKey) {
|
||||||
@@ -377,44 +327,247 @@ class ParamParser {
|
|||||||
}
|
}
|
||||||
return undefined;
|
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 = {
|
type GetBuildQueryPluginArgs = {
|
||||||
collectionSlug?: string
|
collectionSlug?: string
|
||||||
globalSlug?: string
|
versionsFields?: Field[]
|
||||||
isGlobalModel?: boolean
|
|
||||||
isVersionsModel?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BuildQueryArgs = {
|
export type BuildQueryArgs = {
|
||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
where: Where
|
where: Where
|
||||||
overrideAccess: boolean
|
overrideAccess: boolean
|
||||||
|
globalSlug?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// This plugin asynchronously builds a list of Mongoose query constraints
|
// This plugin asynchronously builds a list of Mongoose query constraints
|
||||||
// which can then be used in subsequent Mongoose queries.
|
// which can then be used in subsequent Mongoose queries.
|
||||||
const getBuildQueryPlugin = ({
|
const getBuildQueryPlugin = ({
|
||||||
collectionSlug,
|
collectionSlug,
|
||||||
globalSlug,
|
versionsFields,
|
||||||
isGlobalModel,
|
}: GetBuildQueryPluginArgs = {}) => {
|
||||||
isVersionsModel,
|
|
||||||
}: GetBuildQueryPluginArgs) => {
|
|
||||||
return function buildQueryPlugin(schema) {
|
return function buildQueryPlugin(schema) {
|
||||||
const modifiedSchema = schema;
|
const modifiedSchema = schema;
|
||||||
async function buildQuery({ req, where, overrideAccess = false }: BuildQueryArgs) {
|
async function buildQuery({ req, where, overrideAccess = false, globalSlug }: BuildQueryArgs): Promise<Record<string, unknown>> {
|
||||||
const paramParser = new ParamParser({
|
const paramParser = new ParamParser({
|
||||||
req,
|
req,
|
||||||
collectionSlug,
|
collectionSlug,
|
||||||
globalSlug,
|
globalSlug,
|
||||||
isGlobalModel,
|
versionsFields,
|
||||||
isVersionsModel,
|
|
||||||
model: this,
|
model: this,
|
||||||
where,
|
where,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
});
|
});
|
||||||
const params = await paramParser.parse();
|
const result = await paramParser.parse();
|
||||||
return params.searchParams;
|
// TODO: throw errors here
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
modifiedSchema.statics.buildQuery = buildQuery;
|
modifiedSchema.statics.buildQuery = buildQuery;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,27 +1,40 @@
|
|||||||
import mongoose, { SchemaType } from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { createArrayFromCommaDelineated } from './createArrayFromCommaDelineated';
|
import { createArrayFromCommaDelineated } from './createArrayFromCommaDelineated';
|
||||||
import { getSchemaTypeOptions } from './getSchemaTypeOptions';
|
|
||||||
import wordBoundariesRegex from '../utilities/wordBoundariesRegex';
|
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;
|
let formattedValue = val;
|
||||||
const schemaOptions = getSchemaTypeOptions(schemaType);
|
|
||||||
|
|
||||||
// Disregard invalid _ids
|
// Disregard invalid _ids
|
||||||
|
|
||||||
if (path === '_id' && typeof val === 'string' && val.split(',').length === 1) {
|
if (path === '_id' && typeof val === 'string' && val.split(',').length === 1) {
|
||||||
if (schemaType?.instance === 'ObjectID') {
|
if (!hasCustomID) {
|
||||||
const isValid = mongoose.Types.ObjectId.isValid(val);
|
const isValid = mongoose.Types.ObjectId.isValid(val);
|
||||||
|
|
||||||
|
formattedValue = new mongoose.Types.ObjectId(val);
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
|
ctx.errors.push({ path });
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (schemaType?.instance === 'Number') {
|
if (field.type === 'number') {
|
||||||
const parsedNumber = parseFloat(val);
|
const parsedNumber = parseFloat(val);
|
||||||
|
|
||||||
if (Number.isNaN(parsedNumber)) {
|
if (Number.isNaN(parsedNumber)) {
|
||||||
|
ctx.errors.push({ path });
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,17 +42,34 @@ export const sanitizeQueryValue = (schemaType: SchemaType, path: string, operato
|
|||||||
|
|
||||||
// Cast incoming values as proper searchable types
|
// 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() === 'true') formattedValue = true;
|
||||||
if (val.toLowerCase() === 'false') formattedValue = false;
|
if (val.toLowerCase() === 'false') formattedValue = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (schemaType?.instance === 'Number' && typeof val === 'string') {
|
if (field.type === 'number' && typeof val === 'string') {
|
||||||
formattedValue = Number(val);
|
formattedValue = Number(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((schemaOptions?.ref || schemaOptions?.refPath) && val === 'null') {
|
if (['relationship', 'upload'].includes(field.type) && val === 'null') {
|
||||||
formattedValue = null;
|
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
|
// Set up specific formatting necessary by operators
|
||||||
@@ -74,23 +104,6 @@ export const sanitizeQueryValue = (schemaType: SchemaType, path: string, operato
|
|||||||
formattedValue = createArrayFromCommaDelineated(formattedValue);
|
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 (path !== '_id') {
|
||||||
if (operator === 'contains') {
|
if (operator === 'contains') {
|
||||||
formattedValue = { $regex: formattedValue, $options: 'i' };
|
formattedValue = { $regex: formattedValue, $options: 'i' };
|
||||||
@@ -1,23 +1,20 @@
|
|||||||
import { Access } from '../config/types';
|
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 { FieldAccess, tabHasName } from '../fields/config/types';
|
||||||
import type { CollectionConfig } from '../collections/config/types';
|
import type { SanitizedCollectionConfig } from '../collections/config/types';
|
||||||
import type { GlobalConfig } from '../globals/config/types';
|
import { TypeWithID } from '../collections/config/types';
|
||||||
|
import type { SanitizedGlobalConfig } from '../globals/config/types';
|
||||||
import type { PayloadRequest } from '../express/types';
|
import type { PayloadRequest } from '../express/types';
|
||||||
import type { CollectionPermission, GlobalPermission } from '../auth/types';
|
import type { CollectionPermission, GlobalPermission } from '../auth/types';
|
||||||
import { TypeWithID } from '../collections/config/types';
|
|
||||||
|
|
||||||
type Args = ({
|
type Args = {
|
||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
operations: AllOperations[]
|
operations: AllOperations[]
|
||||||
id?: string
|
id?: string
|
||||||
} & ({
|
type: 'collection' | 'global'
|
||||||
type: 'collection'
|
entity: SanitizedCollectionConfig | SanitizedGlobalConfig
|
||||||
entity: CollectionConfig
|
}
|
||||||
} | {
|
|
||||||
type: 'global'
|
|
||||||
entity: GlobalConfig
|
|
||||||
}))
|
|
||||||
type ReturnType<T extends Args> = T['type'] extends 'global' ? [GlobalPermission, Promise<void>[]] : [CollectionPermission, Promise<void>[]]
|
type ReturnType<T extends Args> = T['type'] extends 'global' ? [GlobalPermission, Promise<void>[]] : [CollectionPermission, Promise<void>[]]
|
||||||
|
|
||||||
type CreateAccessPromise = (args: {
|
type CreateAccessPromise = (args: {
|
||||||
@@ -111,14 +108,15 @@ export function getEntityPolicies<T extends Args>(args: T): ReturnType<T> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const executeFieldPolicies = ({
|
const executeFieldPolicies = async ({
|
||||||
policiesObj = {},
|
policiesObj,
|
||||||
fields,
|
fields,
|
||||||
operation,
|
operation,
|
||||||
|
entityAccessPromise,
|
||||||
}) => {
|
}) => {
|
||||||
const mutablePolicies = policiesObj;
|
const mutablePolicies = policiesObj.fields;
|
||||||
|
|
||||||
fields.forEach((field) => {
|
fields.forEach(async (field) => {
|
||||||
if (field.name) {
|
if (field.name) {
|
||||||
if (!mutablePolicies[field.name]) mutablePolicies[field.name] = {};
|
if (!mutablePolicies[field.name]) mutablePolicies[field.name] = {};
|
||||||
|
|
||||||
@@ -131,17 +129,19 @@ export function getEntityPolicies<T extends Args>(args: T): ReturnType<T> {
|
|||||||
accessLevel: 'field',
|
accessLevel: 'field',
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
|
if (entityAccessPromise) await entityAccessPromise;
|
||||||
mutablePolicies[field.name][operation] = {
|
mutablePolicies[field.name][operation] = {
|
||||||
permission: isLoggedIn,
|
permission: policiesObj[operation]?.permission,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.fields) {
|
if (field.fields) {
|
||||||
if (!mutablePolicies[field.name].fields) mutablePolicies[field.name].fields = {};
|
if (!mutablePolicies[field.name].fields) mutablePolicies[field.name].fields = {};
|
||||||
executeFieldPolicies({
|
executeFieldPolicies({
|
||||||
policiesObj: mutablePolicies[field.name].fields,
|
policiesObj: mutablePolicies[field.name],
|
||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
operation,
|
operation,
|
||||||
|
entityAccessPromise,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (field.fields) {
|
} else if (field.fields) {
|
||||||
@@ -149,6 +149,7 @@ export function getEntityPolicies<T extends Args>(args: T): ReturnType<T> {
|
|||||||
policiesObj: mutablePolicies,
|
policiesObj: mutablePolicies,
|
||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
operation,
|
operation,
|
||||||
|
entityAccessPromise,
|
||||||
});
|
});
|
||||||
} else if (field.type === 'tabs') {
|
} else if (field.type === 'tabs') {
|
||||||
field.tabs.forEach((tab) => {
|
field.tabs.forEach((tab) => {
|
||||||
@@ -158,12 +159,14 @@ export function getEntityPolicies<T extends Args>(args: T): ReturnType<T> {
|
|||||||
policiesObj: mutablePolicies[tab.name].fields,
|
policiesObj: mutablePolicies[tab.name].fields,
|
||||||
fields: tab.fields,
|
fields: tab.fields,
|
||||||
operation,
|
operation,
|
||||||
|
entityAccessPromise,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
executeFieldPolicies({
|
executeFieldPolicies({
|
||||||
policiesObj: mutablePolicies,
|
policiesObj: mutablePolicies,
|
||||||
fields: tab.fields,
|
fields: tab.fields,
|
||||||
operation,
|
operation,
|
||||||
|
entityAccessPromise,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -172,24 +175,28 @@ export function getEntityPolicies<T extends Args>(args: T): ReturnType<T> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
operations.forEach((operation) => {
|
operations.forEach((operation) => {
|
||||||
executeFieldPolicies({
|
let entityAccessPromise: Promise<void>;
|
||||||
policiesObj: policies.fields,
|
|
||||||
fields: entity.fields,
|
|
||||||
operation,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (typeof entity.access[operation] === 'function') {
|
if (typeof entity.access[operation] === 'function') {
|
||||||
promises.push(createAccessPromise({
|
entityAccessPromise = createAccessPromise({
|
||||||
policiesObj: policies,
|
policiesObj: policies,
|
||||||
access: entity.access[operation],
|
access: entity.access[operation],
|
||||||
operation,
|
operation,
|
||||||
accessLevel: 'entity',
|
accessLevel: 'entity',
|
||||||
}));
|
});
|
||||||
|
promises.push(entityAccessPromise);
|
||||||
} else {
|
} else {
|
||||||
policies[operation] = {
|
policies[operation] = {
|
||||||
permission: isLoggedIn,
|
permission: isLoggedIn,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
executeFieldPolicies({
|
||||||
|
policiesObj: policies,
|
||||||
|
fields: entity.fields,
|
||||||
|
operation,
|
||||||
|
entityAccessPromise,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return [policies, promises] as ReturnType<T>;
|
return [policies, promises] as ReturnType<T>;
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ const replaceWithDraftIfAvailable = async <T extends TypeWithID>({
|
|||||||
where: queryToBuild,
|
where: queryToBuild,
|
||||||
req,
|
req,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
globalSlug: entityType === 'global' ? entity.slug : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
let draft = await VersionModel.findOne(query, {}, {
|
let draft = await VersionModel.findOne(query, {}, {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { buildConfig } from '../buildConfig';
|
|||||||
export const slug = 'global';
|
export const slug = 'global';
|
||||||
export const arraySlug = 'array';
|
export const arraySlug = 'array';
|
||||||
|
|
||||||
|
export const accessControlSlug = 'access-control';
|
||||||
|
|
||||||
export const englishLocale = 'en';
|
export const englishLocale = 'en';
|
||||||
export const spanishLocale = 'es';
|
export const spanishLocale = 'es';
|
||||||
|
|
||||||
@@ -51,6 +53,33 @@ export default buildConfig({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
slug: accessControlSlug,
|
||||||
|
access: {
|
||||||
|
read: ({ req: { user } }) => {
|
||||||
|
if (user) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled: {
|
||||||
|
equals: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'enabled',
|
||||||
|
type: 'checkbox',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
onInit: async (payload) => {
|
onInit: async (payload) => {
|
||||||
await payload.create({
|
await payload.create({
|
||||||
@@ -60,5 +89,12 @@ export default buildConfig({
|
|||||||
password: devUser.password,
|
password: devUser.password,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await payload.updateGlobal({
|
||||||
|
slug: accessControlSlug,
|
||||||
|
data: {
|
||||||
|
title: 'hello',
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { GraphQLClient } from 'graphql-request';
|
import { GraphQLClient } from 'graphql-request';
|
||||||
import { initPayloadTest } from '../helpers/configHelpers';
|
import { initPayloadTest } from '../helpers/configHelpers';
|
||||||
import configPromise, { arraySlug, englishLocale, slug, spanishLocale } from './config';
|
import configPromise, { accessControlSlug, arraySlug, englishLocale, slug, spanishLocale } from './config';
|
||||||
import payload from '../../src';
|
import payload from '../../src';
|
||||||
import { RESTClient } from '../helpers/rest';
|
import { RESTClient } from '../helpers/rest';
|
||||||
|
|
||||||
@@ -144,6 +144,29 @@ describe('globals', () => {
|
|||||||
expect(en).toMatchObject(localized.en);
|
expect(en).toMatchObject(localized.en);
|
||||||
expect(es).toMatchObject(localized.es);
|
expect(es).toMatchObject(localized.es);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should respect valid access query constraint', async () => {
|
||||||
|
const emptyGlobal = await payload.findGlobal({
|
||||||
|
slug: accessControlSlug,
|
||||||
|
overrideAccess: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(Object.keys(emptyGlobal)).toHaveLength(0);
|
||||||
|
|
||||||
|
await payload.updateGlobal({
|
||||||
|
slug: accessControlSlug,
|
||||||
|
data: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasAccess = await payload.findGlobal({
|
||||||
|
slug: accessControlSlug,
|
||||||
|
overrideAccess: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(hasAccess.title).toBeDefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('graphql', () => {
|
describe('graphql', () => {
|
||||||
|
|||||||
@@ -5,11 +5,7 @@ import payload from '../../src';
|
|||||||
import type {
|
import type {
|
||||||
LocalizedPost,
|
LocalizedPost,
|
||||||
WithLocalizedRelationship,
|
WithLocalizedRelationship,
|
||||||
LocalizedRequired,
|
|
||||||
RelationshipLocalized,
|
|
||||||
GlobalArray,
|
|
||||||
} from './payload-types';
|
} from './payload-types';
|
||||||
import type { LocalizedPostAllLocale } from './config';
|
|
||||||
import configPromise, { relationshipLocalizedSlug, slug, withLocalizedRelSlug, withRequiredLocalizedFields } from './config';
|
import configPromise, { relationshipLocalizedSlug, slug, withLocalizedRelSlug, withRequiredLocalizedFields } from './config';
|
||||||
import {
|
import {
|
||||||
defaultLocale,
|
defaultLocale,
|
||||||
|
|||||||
Reference in New Issue
Block a user