Skip to content

Commit

Permalink
feat(api-headless-cms): enable skipping validator by name (#3582)
Browse files Browse the repository at this point in the history
  • Loading branch information
brunozoric committed Oct 23, 2023
1 parent 7f45c6e commit e83640f
Show file tree
Hide file tree
Showing 15 changed files with 1,087 additions and 97 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { CmsApiModel } from "~/plugins";

const fields = /* GraphQL */ `
id
entryId
savedOn
createdOn
createdBy {
id
displayName
type
}
meta {
title
version
}
`;

const ERROR = `
error {
message
data
code
stack
}
`;

export const validateMutation = (model: Pick<CmsApiModel, "singularApiName">) => {
return /* GraphQL */ `
mutation ValidateProduct($revision: ID, $data: ${model.singularApiName}Input!) {
validate: validate${model.singularApiName}(revision: $revision, data: $data) {
data {
id
fieldId
error
parents
}
${ERROR}
}
}
`;
};

export const createMutation = (model: Pick<CmsApiModel, "singularApiName">) => {
return /* GraphQL */ `
mutation CreateProduct($data: ${model.singularApiName}Input!, $options: CreateCmsEntryOptionsInput) {
create: create${model.singularApiName}(data: $data, options: $options) {
data {
${fields}
}
${ERROR}
}
}
`;
};

export const createRevisionMutation = (model: Pick<CmsApiModel, "singularApiName">) => {
return /* GraphQL */ `
mutation CreateProductFrom($revision: ID!, $options: CreateRevisionCmsEntryOptionsInput) {
createRevision: create${model.singularApiName}From(revision: $revision, options: $options) {
data {
${fields}
}
${ERROR}
}
}
`;
};

export const updateMutation = (model: Pick<CmsApiModel, "singularApiName">) => {
return /* GraphQL */ `
mutation UpdateProduct($revision: ID!, $data: ${model.singularApiName}Input!, $options: UpdateCmsEntryOptionsInput) {
update: update${model.singularApiName}(revision: $revision, data: $data, options: $options) {
data {
${fields}
}
${ERROR}
}
}
`;
};

export const deleteMutation = (model: Pick<CmsApiModel, "singularApiName">) => {
return /* GraphQL */ `
mutation DeleteProduct($revision: ID!) {
delete: delete${model.singularApiName}(revision: $revision) {
data
${ERROR}
}
}
`;
};

export const publishMutation = (model: Pick<CmsApiModel, "singularApiName">) => {
return /* GraphQL */ `
mutation UpdateProduct($revision: ID!) {
publish: publish${model.singularApiName}(revision: $revision) {
data {
${fields}
}
${ERROR}
}
}
`;
};
Original file line number Diff line number Diff line change
@@ -1,30 +1,12 @@
import { GraphQLHandlerParams, useGraphQLHandler } from "~tests/testHelpers/useGraphQLHandler";
import { CmsApiModel } from "~/plugins";

const ERROR = `
error {
message
data
code
stack
}
`;

const validateMutation = (model: Pick<CmsApiModel, "singularApiName">) => {
return /* GraphQL */ `
mutation ValidateProduct($revision: ID, $data: ${model.singularApiName}Input!) {
validate: validate${model.singularApiName}(revision: $revision, data: $data) {
data {
id
fieldId
error
parents
}
${ERROR}
}
}
`;
};
import {
validateMutation,
createMutation,
updateMutation,
createRevisionMutation,
publishMutation
} from "./handler.graphql";

interface Params extends Partial<GraphQLHandlerParams> {
model: Pick<CmsApiModel, "singularApiName">;
Expand All @@ -46,6 +28,42 @@ export const useValidationManageHandler = (params: Params) => {
},
headers
});
},
async create(variables: Record<string, any>, headers: Record<string, any> = {}) {
return await contentHandler.invoke({
body: {
query: createMutation(params.model),
variables
},
headers
});
},
async createRevision(variables: Record<string, any>, headers: Record<string, any> = {}) {
return await contentHandler.invoke({
body: {
query: createRevisionMutation(params.model),
variables
},
headers
});
},
async update(variables: Record<string, any>, headers: Record<string, any> = {}) {
return await contentHandler.invoke({
body: {
query: updateMutation(params.model),
variables
},
headers
});
},
async publish(variables: Record<string, any>, headers: Record<string, any> = {}) {
return await contentHandler.invoke({
body: {
query: publishMutation(params.model),
variables
},
headers
});
}
};
};
47 changes: 22 additions & 25 deletions packages/api-headless-cms/src/crud/contentEntry.crud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ import {
UpdateCmsEntryInput
} from "~/types";
import {
validateModelEntryDataOrThrow,
validateModelEntryData
validateModelEntryData,
validateModelEntryDataOrThrow
} from "./contentEntry/entryDataValidation";
import { SecurityIdentity } from "@webiny/api-security/types";
import { createTopic } from "@webiny/pubsub";
Expand Down Expand Up @@ -658,13 +658,12 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm
*/
const initialInput = mapAndCleanCreateInputData(model, inputData);

if (options?.validate !== false) {
await validateModelEntryDataOrThrow({
context,
model,
data: initialInput
});
}
await validateModelEntryDataOrThrow({
context,
model,
data: initialInput,
skipValidators: options?.skipValidators
});

const input = await referenceFieldsMapping({
context,
Expand Down Expand Up @@ -792,14 +791,13 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm
...input
};

if (options?.validate !== false) {
await validateModelEntryDataOrThrow({
context,
model,
data: initialValues,
entry: originalEntry
});
}
await validateModelEntryDataOrThrow({
context,
model,
data: initialValues,
entry: originalEntry,
skipValidators: options?.skipValidators
});

const values = await referenceFieldsMapping({
context,
Expand Down Expand Up @@ -916,14 +914,13 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm

const originalEntry = await entryFromStorageTransform(context, model, originalStorageEntry);

if (options?.validate !== false) {
await validateModelEntryDataOrThrow({
context,
model,
data: input,
entry: originalEntry
});
}
await validateModelEntryDataOrThrow({
context,
model,
data: input,
entry: originalEntry,
skipValidators: options?.skipValidators
});

await entriesPermissions.ensure({ owns: originalEntry.createdBy });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
CmsModelFieldValidatorValidateParams
} from "~/types";
import WebinyError from "@webiny/error";
import camelCase from "lodash/camelCase";

type PluginValidationCallable = (params: CmsModelFieldValidatorValidateParams) => Promise<boolean>;
type PluginValidationList = Record<string, PluginValidationCallable[]>;
Expand Down Expand Up @@ -76,6 +77,15 @@ const validatePredefinedValue = (field: CmsModelField, value: any | any[]): stri
}
return "Value sent does not match any of the available predefined values.";
};

const getFieldValidation = (
listValidation?: CmsModelFieldValidation[]
): CmsModelFieldValidation[] => {
if (!listValidation?.length) {
return [];
}
return listValidation.filter(item => item.name !== "dynamicZone");
};
/**
* When multiple values is selected we must run validations on the array containing the values
* And then on each value in the array
Expand All @@ -85,15 +95,19 @@ const runFieldMultipleValuesValidations = async (
): Promise<string | null> => {
const { field, data } = params;
const values = data?.[field.fieldId];
const valuesError = await validateValue(params, field.listValidation || [], values);
const valuesError = await validateValue(
params,
getFieldValidation(field.listValidation),
values
);
if (valuesError) {
return valuesError;
}
if (!values) {
if (values === null || values === undefined) {
return null;
}
for (const value of values) {
const valueError = await validateValue(params, field.validation || [], value);
const valueError = await validateValue(params, getFieldValidation(field.validation), value);
if (valueError) {
return valueError;
}
Expand Down Expand Up @@ -132,25 +146,47 @@ export interface ValidateModelEntryDataParams {
model: CmsModel;
data: InputData;
entry?: CmsEntry;
skipValidators?: string[];
}

export const validateModelEntryData = async (params: ValidateModelEntryDataParams) => {
const { context, model, entry, data } = params;
const { context, model, entry, data, skipValidators } = params;

const isValidatorSkipped = (plugin: CmsModelFieldValidatorPlugin) => {
if (!skipValidators) {
return false;
}
return skipValidators.includes(camelCase(plugin.validator.name));
};

const skippedValidators = new Set<string>();

/**
* To later simplify searching for the validations we map them to a name.
* @see CmsModelFieldValidatorPlugin.validator.validate
*/
const validatorList: PluginValidationList = context.plugins
.byType<CmsModelFieldValidatorPlugin>("cms-model-field-validator")
.reduce((acc, plugin) => {
const name = plugin.validator.name;
if (!acc[name]) {
acc[name] = [];
}
acc[name].push(plugin.validator.validate);

return acc;
}, {} as PluginValidationList);
const validatorList: PluginValidationList = {};
const validators = context.plugins.byType<CmsModelFieldValidatorPlugin>(
"cms-model-field-validator"
);
for (const plugin of validators) {
const name = plugin.validator.name;
if (!validatorList[name]) {
validatorList[name] = [];
}
const isSkipped = isValidatorSkipped(plugin);
if (isSkipped) {
skippedValidators.add(name);
}
validatorList[name].push(isSkipped ? async () => true : plugin.validator.validate);
}
/**
* No point in continuing if all validators are skipped.
*/
const keys = Object.keys(validatorList);
if (keys.length === skippedValidators.size) {
return [];
}

return await validate({
validatorList,
Expand Down
2 changes: 1 addition & 1 deletion packages/api-headless-cms/src/graphql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ import { createBaseSchema } from "~/graphql/schema/baseSchema";

export type CreateGraphQLParams = GraphQLHandlerFactoryParams;
export const createGraphQL = (params: CreateGraphQLParams) => {
return [...createBaseSchema(), createSystemSchemaPlugin(), graphQLHandlerFactory(params)];
return [createBaseSchema(), createSystemSchemaPlugin(), graphQLHandlerFactory(params)];
};

0 comments on commit e83640f

Please sign in to comment.