Skip to content

Commit

Permalink
fix(core): Fix renaming of product with readonly custom field (vendur…
Browse files Browse the repository at this point in the history
  • Loading branch information
Anddrrew authored Feb 20, 2024
1 parent c890d73 commit 2075d6d
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 97 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@ import { CustomFields } from '../../common/generated-types';
import { QueryResult } from '../query-result';
import { ServerConfigService } from '../server-config';
import { addCustomFields } from '../utils/add-custom-fields';
import {
isEntityCreateOrUpdateMutation,
removeReadonlyCustomFields,
} from '../utils/remove-readonly-custom-fields';
import { removeReadonlyCustomFields } from '../utils/remove-readonly-custom-fields';
import { transformRelationCustomFieldInputs } from '../utils/transform-relation-custom-field-inputs';
import { isEntityCreateOrUpdateMutation } from '../utils/is-entity-create-or-update-mutation';

@Injectable()
export class BaseDataService {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { DocumentNode, getOperationAST, NamedTypeNode, TypeNode } from 'graphql';

const CREATE_ENTITY_REGEX = /Create([A-Za-z]+)Input/;
const UPDATE_ENTITY_REGEX = /Update([A-Za-z]+)Input/;

/**
* Checks the current documentNode for an operation with a variable named "Create<Entity>Input" or "Update<Entity>Input"
* and if a match is found, returns the <Entity> name.
*/
export function isEntityCreateOrUpdateMutation(documentNode: DocumentNode): string | undefined {
const operationDef = getOperationAST(documentNode, null);
if (operationDef && operationDef.variableDefinitions) {
for (const variableDef of operationDef.variableDefinitions) {
const namedType = extractInputType(variableDef.type);
const inputTypeName = namedType.name.value;

// special cases which don't follow the usual pattern
if (inputTypeName === 'UpdateActiveAdministratorInput') {
return 'Administrator';
}
if (inputTypeName === 'ModifyOrderInput') {
return 'Order';
}
if (
inputTypeName === 'AddItemToDraftOrderInput' ||
inputTypeName === 'AdjustDraftOrderLineInput'
) {
return 'OrderLine';
}

const createMatch = inputTypeName.match(CREATE_ENTITY_REGEX);
if (createMatch) {
return createMatch[1];
}
const updateMatch = inputTypeName.match(UPDATE_ENTITY_REGEX);
if (updateMatch) {
return updateMatch[1];
}
}
}
}

function extractInputType(type: TypeNode): NamedTypeNode {
if (type.kind === 'NonNullType') {
return extractInputType(type.type);
}
if (type.kind === 'ListType') {
return extractInputType(type.type);
}
return type;
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,23 @@ describe('removeReadonlyCustomFields', () => {
} as any);
});

it('readonly field and customFields is undefined', () => {
const config: CustomFieldConfig[] = [{ name: 'alias', type: 'string', readonly: true, list: false }];

const entity = {
id: 1,
name: 'test',
customFields: undefined,
};

const result = removeReadonlyCustomFields(entity, config);
expect(result).toEqual({
id: 1,
name: 'test',
customFields: undefined,
} as any);
});

it('readonly field in translation', () => {
const config: CustomFieldConfig[] = [
{ name: 'alias', type: 'localeString', readonly: true, list: false },
Expand All @@ -63,6 +80,24 @@ describe('removeReadonlyCustomFields', () => {
} as any);
});

it('readonly field and customFields is undefined in translation', () => {
const config: CustomFieldConfig[] = [
{ name: 'alias', type: 'localeString', readonly: true, list: false },
];
const entity = {
id: 1,
name: 'test',
translations: [{ id: 1, languageCode: LanguageCode.en, customFields: undefined }],
};

const result = removeReadonlyCustomFields(entity, config);
expect(result).toEqual({
id: 1,
name: 'test',
translations: [{ id: 1, languageCode: LanguageCode.en, customFields: undefined }],
} as any);
});

it('wrapped in an input object', () => {
const config: CustomFieldConfig[] = [
{ name: 'weight', type: 'int', list: false },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,121 +1,53 @@
import { simpleDeepClone } from '@vendure/common/lib/simple-deep-clone';
import { DocumentNode, getOperationAST, NamedTypeNode, TypeNode } from 'graphql';

import { CustomFieldConfig } from '../../common/generated-types';

const CREATE_ENTITY_REGEX = /Create([A-Za-z]+)Input/;
const UPDATE_ENTITY_REGEX = /Update([A-Za-z]+)Input/;

type InputWithOptionalCustomFields = Record<string, any> & {
customFields?: Record<string, any>;
};
type InputWithCustomFields = Record<string, any> & {
customFields: Record<string, any>;
};

type EntityInput = InputWithOptionalCustomFields & {
translations?: InputWithOptionalCustomFields[];
};

/**
* Checks the current documentNode for an operation with a variable named "Create<Entity>Input" or "Update<Entity>Input"
* and if a match is found, returns the <Entity> name.
*/
export function isEntityCreateOrUpdateMutation(documentNode: DocumentNode): string | undefined {
const operationDef = getOperationAST(documentNode, null);
if (operationDef && operationDef.variableDefinitions) {
for (const variableDef of operationDef.variableDefinitions) {
const namedType = extractInputType(variableDef.type);
const inputTypeName = namedType.name.value;

// special cases which don't follow the usual pattern
if (inputTypeName === 'UpdateActiveAdministratorInput') {
return 'Administrator';
}
if (inputTypeName === 'ModifyOrderInput') {
return 'Order';
}
if (
inputTypeName === 'AddItemToDraftOrderInput' ||
inputTypeName === 'AdjustDraftOrderLineInput'
) {
return 'OrderLine';
}
type Variable = EntityInput | EntityInput[];

const createMatch = inputTypeName.match(CREATE_ENTITY_REGEX);
if (createMatch) {
return createMatch[1];
}
const updateMatch = inputTypeName.match(UPDATE_ENTITY_REGEX);
if (updateMatch) {
return updateMatch[1];
}
}
}
}

function extractInputType(type: TypeNode): NamedTypeNode {
if (type.kind === 'NonNullType') {
return extractInputType(type.type);
}
if (type.kind === 'ListType') {
return extractInputType(type.type);
}
return type;
}
type WrappedVariable = {
input: Variable;
};

/**
* Removes any `readonly` custom fields from an entity (including its translations).
* To be used before submitting the entity for a create or update request.
*/
export function removeReadonlyCustomFields(
variables: { input?: EntityInput | EntityInput[] } | EntityInput | EntityInput[],
variables: Variable | WrappedVariable | WrappedVariable[],
customFieldConfig: CustomFieldConfig[],
): { input?: EntityInput | EntityInput[] } | EntityInput | EntityInput[] {
if (!Array.isArray(variables)) {
) {
if (Array.isArray(variables)) {
return variables.map(variable => removeReadonlyCustomFields(variable, customFieldConfig));
}

if ('input' in variables && variables.input) {
if (Array.isArray(variables.input)) {
for (const input of variables.input) {
removeReadonly(input, customFieldConfig);
}
variables.input = variables.input.map(variable => removeReadonly(variable, customFieldConfig));
} else {
removeReadonly(variables.input, customFieldConfig);
}
} else {
for (const input of variables) {
removeReadonly(input, customFieldConfig);
variables.input = removeReadonly(variables.input, customFieldConfig);
}
return variables;
}

return removeReadonly(variables, customFieldConfig);
}

function removeReadonly(input: InputWithOptionalCustomFields, customFieldConfig: CustomFieldConfig[]) {
for (const field of customFieldConfig) {
if (field.readonly) {
if (field.type === 'localeString') {
if (hasTranslations(input)) {
for (const translation of input.translations) {
if (
hasCustomFields(translation) &&
translation.customFields[field.name] !== undefined
) {
delete translation.customFields[field.name];
}
}
}
} else {
if (hasCustomFields(input) && input.customFields[field.name] !== undefined) {
delete input.customFields[field.name];
}
}
}
}
return input;
}
function removeReadonly(input: EntityInput, customFieldConfig: CustomFieldConfig[]) {
const readonlyConfigs = customFieldConfig.filter(({ readonly }) => readonly);

function hasCustomFields(input: any): input is InputWithCustomFields {
return input != null && input.hasOwnProperty('customFields');
}
readonlyConfigs.forEach(({ name }) => {
input.translations?.forEach(translation => {
delete translation.customFields?.[name];
});

delete input.customFields?.[name];
});

function hasTranslations(input: any): input is { translations: InputWithOptionalCustomFields[] } {
return input != null && input.hasOwnProperty('translations');
return input;
}

0 comments on commit 2075d6d

Please sign in to comment.