[permissions] Update permission check layer (#13485)
Fixes https://github.com/twentyhq/core-team-issues/issues/1262 In this PR we add the update permission check layer by - for the graphql api: extracting columns to update from the expressionMap - for rest api: .save() is used so we need to add the permission layer to .save directly. We also take advantage of this PR to filter out non-readable fields from save response (other save returns the whole entity) - this was planned in https://github.com/twentyhq/core-team-issues/issues/1216 The current solution does not work with rest api depth 2 queries, but this seem to already not work on main (for timeout reasons though, so different). I offer to create a ticket to fix it altogether later.
This commit is contained in:
+265
@@ -0,0 +1,265 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { getColumnNameToFieldMetadataIdMap } from 'src/engine/twenty-orm/utils/get-column-name-to-field-metadata-id.util';
|
||||
|
||||
describe('getColumnNameToFieldMetadataIdMap', () => {
|
||||
const createMockObjectMetadataItemWithFieldMaps = (
|
||||
fieldsById: Record<string, any>,
|
||||
): ObjectMetadataItemWithFieldMaps =>
|
||||
({
|
||||
id: 'test-object-id',
|
||||
nameSingular: 'test',
|
||||
namePlural: 'tests',
|
||||
labelSingular: 'Test',
|
||||
labelPlural: 'Tests',
|
||||
description: 'Test object',
|
||||
icon: 'IconTest',
|
||||
targetTableName: 'test',
|
||||
isCustom: false,
|
||||
isRemote: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isAuditLogged: false,
|
||||
isSearchable: false,
|
||||
labelIdentifierFieldMetadataId: '',
|
||||
imageIdentifierFieldMetadataId: '',
|
||||
workspaceId: 'test-workspace-id',
|
||||
indexMetadatas: [],
|
||||
fieldsById,
|
||||
fieldIdByName: {},
|
||||
fieldIdByJoinColumnName: {},
|
||||
}) as unknown as ObjectMetadataItemWithFieldMaps;
|
||||
|
||||
const createMockFieldMetadata = (
|
||||
id: string,
|
||||
name: string,
|
||||
type: FieldMetadataType,
|
||||
) => ({
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
label: name,
|
||||
objectMetadataId: 'test-object-id',
|
||||
isLabelSyncedWithName: true,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: 'test-workspace-id',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
describe('with simple field types', () => {
|
||||
it('should return a map with column name to field metadata id for simple field types', () => {
|
||||
const fieldsById = {
|
||||
'field-1': createMockFieldMetadata(
|
||||
'field-1',
|
||||
'name',
|
||||
FieldMetadataType.TEXT,
|
||||
),
|
||||
'field-2': createMockFieldMetadata(
|
||||
'field-2',
|
||||
'age',
|
||||
FieldMetadataType.NUMBER,
|
||||
),
|
||||
};
|
||||
|
||||
const objectMetadataItemWithFieldMaps =
|
||||
createMockObjectMetadataItemWithFieldMaps(fieldsById);
|
||||
|
||||
const result = getColumnNameToFieldMetadataIdMap(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
);
|
||||
|
||||
expect(result['name']).toBe('field-1');
|
||||
expect(result['age']).toBe('field-2');
|
||||
expect(Object.keys(result)).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with composite field types', () => {
|
||||
it('should return column names to field metadata id for FULL_NAME composite type', () => {
|
||||
const fieldsById = {
|
||||
'field-1': createMockFieldMetadata(
|
||||
'field-1',
|
||||
'fullName',
|
||||
FieldMetadataType.FULL_NAME,
|
||||
),
|
||||
};
|
||||
|
||||
const objectMetadataItemWithFieldMaps =
|
||||
createMockObjectMetadataItemWithFieldMaps(fieldsById);
|
||||
|
||||
const result = getColumnNameToFieldMetadataIdMap(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
);
|
||||
|
||||
expect(result['fullNameFirstName']).toBe('field-1');
|
||||
expect(result['fullNameLastName']).toBe('field-1');
|
||||
expect(Object.keys(result)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should return column names to field metadata id for CURRENCY composite type', () => {
|
||||
const fieldsById = {
|
||||
'field-1': createMockFieldMetadata(
|
||||
'field-1',
|
||||
'price',
|
||||
FieldMetadataType.CURRENCY,
|
||||
),
|
||||
};
|
||||
|
||||
const objectMetadataItemWithFieldMaps =
|
||||
createMockObjectMetadataItemWithFieldMaps(fieldsById);
|
||||
|
||||
const result = getColumnNameToFieldMetadataIdMap(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
);
|
||||
|
||||
expect(result['priceAmountMicros']).toBe('field-1');
|
||||
expect(result['priceCurrencyCode']).toBe('field-1');
|
||||
expect(Object.keys(result)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should handle multiple composite fields', () => {
|
||||
const fieldsById = {
|
||||
'field-1': createMockFieldMetadata(
|
||||
'field-1',
|
||||
'fullName',
|
||||
FieldMetadataType.FULL_NAME,
|
||||
),
|
||||
'field-2': createMockFieldMetadata(
|
||||
'field-2',
|
||||
'price',
|
||||
FieldMetadataType.CURRENCY,
|
||||
),
|
||||
'field-3': createMockFieldMetadata(
|
||||
'field-3',
|
||||
'name',
|
||||
FieldMetadataType.TEXT,
|
||||
),
|
||||
};
|
||||
|
||||
const objectMetadataItemWithFieldMaps =
|
||||
createMockObjectMetadataItemWithFieldMaps(fieldsById);
|
||||
|
||||
const result = getColumnNameToFieldMetadataIdMap(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
);
|
||||
|
||||
expect(result['fullNameFirstName']).toBe('field-1');
|
||||
expect(result['fullNameLastName']).toBe('field-1');
|
||||
expect(result['priceAmountMicros']).toBe('field-2');
|
||||
expect(result['priceCurrencyCode']).toBe('field-2');
|
||||
expect(result['name']).toBe('field-3');
|
||||
expect(Object.keys(result)).toHaveLength(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with mixed field types', () => {
|
||||
it('should handle both simple and composite field types', () => {
|
||||
const fieldsById = {
|
||||
'field-1': createMockFieldMetadata(
|
||||
'field-1',
|
||||
'name',
|
||||
FieldMetadataType.TEXT,
|
||||
),
|
||||
'field-2': createMockFieldMetadata(
|
||||
'field-2',
|
||||
'fullName',
|
||||
FieldMetadataType.FULL_NAME,
|
||||
),
|
||||
'field-3': createMockFieldMetadata(
|
||||
'field-3',
|
||||
'age',
|
||||
FieldMetadataType.NUMBER,
|
||||
),
|
||||
'field-4': createMockFieldMetadata(
|
||||
'field-4',
|
||||
'price',
|
||||
FieldMetadataType.CURRENCY,
|
||||
),
|
||||
};
|
||||
|
||||
const objectMetadataItemWithFieldMaps =
|
||||
createMockObjectMetadataItemWithFieldMaps(fieldsById);
|
||||
|
||||
const result = getColumnNameToFieldMetadataIdMap(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
);
|
||||
|
||||
expect(result['name']).toBe('field-1');
|
||||
expect(result['fullNameFirstName']).toBe('field-2');
|
||||
expect(result['fullNameLastName']).toBe('field-2');
|
||||
expect(result['age']).toBe('field-3');
|
||||
expect(result['priceAmountMicros']).toBe('field-4');
|
||||
expect(result['priceCurrencyCode']).toBe('field-4');
|
||||
expect(Object.keys(result)).toHaveLength(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with relation field types', () => {
|
||||
it('should handle relation field types with join column name', () => {
|
||||
const fieldsById = {
|
||||
'field-1': {
|
||||
...createMockFieldMetadata(
|
||||
'field-1',
|
||||
'company',
|
||||
FieldMetadataType.RELATION,
|
||||
),
|
||||
settings: {
|
||||
relationType: 'ONE_TO_ONE',
|
||||
joinColumnName: 'companyId',
|
||||
},
|
||||
},
|
||||
'field-2': createMockFieldMetadata(
|
||||
'field-2',
|
||||
'name',
|
||||
FieldMetadataType.TEXT,
|
||||
),
|
||||
};
|
||||
|
||||
const objectMetadataItemWithFieldMaps =
|
||||
createMockObjectMetadataItemWithFieldMaps(fieldsById);
|
||||
|
||||
const result = getColumnNameToFieldMetadataIdMap(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
);
|
||||
|
||||
expect(result['companyId']).toBe('field-1');
|
||||
expect(result['name']).toBe('field-2');
|
||||
expect(Object.keys(result)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should skip ONE_TO_MANY relation field types', () => {
|
||||
const fieldsById = {
|
||||
'field-1': {
|
||||
...createMockFieldMetadata(
|
||||
'field-1',
|
||||
'employees',
|
||||
FieldMetadataType.RELATION,
|
||||
),
|
||||
settings: {
|
||||
relationType: 'ONE_TO_MANY',
|
||||
joinColumnName: 'companyId',
|
||||
},
|
||||
},
|
||||
'field-2': createMockFieldMetadata(
|
||||
'field-2',
|
||||
'name',
|
||||
FieldMetadataType.TEXT,
|
||||
),
|
||||
};
|
||||
|
||||
const objectMetadataItemWithFieldMaps =
|
||||
createMockObjectMetadataItemWithFieldMaps(fieldsById);
|
||||
|
||||
const result = getColumnNameToFieldMetadataIdMap(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
);
|
||||
|
||||
expect(result['name']).toBe('field-2');
|
||||
expect(result['companyId']).toBeUndefined();
|
||||
expect(Object.keys(result)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
|
||||
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import {
|
||||
ColumnNameProcessor,
|
||||
processFieldMetadataForColumnNameMapping,
|
||||
} from 'src/engine/twenty-orm/utils/process-field-metadata-for-column-name-mapping.util';
|
||||
|
||||
export function getColumnNameToFieldMetadataIdMap(
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
||||
) {
|
||||
const columnNameToFieldMetadataIdMap: Record<string, string> = {};
|
||||
|
||||
const processor: ColumnNameProcessor = {
|
||||
processCompositeField: ({
|
||||
fieldMetadataId,
|
||||
fieldMetadata,
|
||||
compositeType,
|
||||
}: {
|
||||
fieldMetadataId: string;
|
||||
fieldMetadata: FieldMetadataEntity;
|
||||
compositeType: CompositeType;
|
||||
}) => {
|
||||
compositeType.properties.forEach((compositeProperty) => {
|
||||
const columnName = computeCompositeColumnName(
|
||||
fieldMetadata.name,
|
||||
compositeProperty,
|
||||
);
|
||||
|
||||
columnNameToFieldMetadataIdMap[columnName] = fieldMetadataId;
|
||||
});
|
||||
},
|
||||
processRelationField: ({
|
||||
fieldMetadataId,
|
||||
columnName,
|
||||
}: {
|
||||
fieldMetadataId: string;
|
||||
columnName: string;
|
||||
}) => {
|
||||
columnNameToFieldMetadataIdMap[columnName] = fieldMetadataId;
|
||||
},
|
||||
processSimpleField: ({
|
||||
fieldMetadataId,
|
||||
columnName,
|
||||
}: {
|
||||
fieldMetadataId: string;
|
||||
columnName: string;
|
||||
}) => {
|
||||
columnNameToFieldMetadataIdMap[columnName] = fieldMetadataId;
|
||||
},
|
||||
};
|
||||
|
||||
processFieldMetadataForColumnNameMapping(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
processor,
|
||||
);
|
||||
|
||||
return columnNameToFieldMetadataIdMap;
|
||||
}
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import {
|
||||
computeColumnName,
|
||||
computeCompositeColumnName,
|
||||
} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
|
||||
export function getFieldMetadataIdForColumnNameMap(
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
||||
) {
|
||||
const columnNameToFieldMetadataIdMap: Record<string, string> = {};
|
||||
|
||||
for (const [fieldMetadataId, fieldMetadata] of Object.entries(
|
||||
objectMetadataItemWithFieldMaps.fieldsById,
|
||||
)) {
|
||||
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
|
||||
|
||||
if (!compositeType) {
|
||||
throw new InternalServerError(
|
||||
`Composite type not found for field metadata type ${fieldMetadata.type}`,
|
||||
);
|
||||
}
|
||||
|
||||
compositeType.properties.forEach((compositeProperty) => {
|
||||
const columnName = computeCompositeColumnName(
|
||||
fieldMetadata.name,
|
||||
compositeProperty,
|
||||
);
|
||||
|
||||
columnNameToFieldMetadataIdMap[columnName] = fieldMetadataId;
|
||||
});
|
||||
} else {
|
||||
const columnName = computeColumnName(fieldMetadata, {
|
||||
isForeignKey: fieldMetadata.type === FieldMetadataType.RELATION,
|
||||
});
|
||||
|
||||
columnNameToFieldMetadataIdMap[columnName] = fieldMetadataId;
|
||||
}
|
||||
}
|
||||
|
||||
return columnNameToFieldMetadataIdMap;
|
||||
}
|
||||
+44
-24
@@ -1,29 +1,28 @@
|
||||
import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import {
|
||||
computeColumnName,
|
||||
computeCompositeColumnName,
|
||||
} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
|
||||
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import {
|
||||
ColumnNameProcessor,
|
||||
processFieldMetadataForColumnNameMapping,
|
||||
} from 'src/engine/twenty-orm/utils/process-field-metadata-for-column-name-mapping.util';
|
||||
|
||||
export function getFieldMetadataIdToColumnNamesMap(
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
||||
) {
|
||||
const fieldMetadataToColumnNamesMap = new Map<string, string[]>();
|
||||
|
||||
for (const [fieldMetadataId, fieldMetadata] of Object.entries(
|
||||
objectMetadataItemWithFieldMaps.fieldsById,
|
||||
)) {
|
||||
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
|
||||
|
||||
if (!compositeType) {
|
||||
throw new InternalServerError(
|
||||
`Composite type not found for field metadata type ${fieldMetadata.type}`,
|
||||
);
|
||||
}
|
||||
|
||||
const processor: ColumnNameProcessor = {
|
||||
processCompositeField: ({
|
||||
fieldMetadataId,
|
||||
fieldMetadata,
|
||||
compositeType,
|
||||
}: {
|
||||
fieldMetadataId: string;
|
||||
fieldMetadata: FieldMetadataEntity;
|
||||
compositeType: CompositeType;
|
||||
}) => {
|
||||
compositeType.properties.forEach((compositeProperty) => {
|
||||
const columnName = computeCompositeColumnName(
|
||||
fieldMetadata.name,
|
||||
@@ -38,12 +37,33 @@ export function getFieldMetadataIdToColumnNamesMap(
|
||||
columnName,
|
||||
]);
|
||||
});
|
||||
} else {
|
||||
const columnName = computeColumnName(fieldMetadata);
|
||||
|
||||
},
|
||||
processRelationField: ({
|
||||
fieldMetadataId,
|
||||
columnName,
|
||||
}: {
|
||||
fieldMetadataId: string;
|
||||
fieldMetadata: FieldMetadataEntity;
|
||||
columnName: string;
|
||||
}) => {
|
||||
fieldMetadataToColumnNamesMap.set(fieldMetadataId, [columnName]); // TODO test
|
||||
},
|
||||
processSimpleField: ({
|
||||
fieldMetadataId,
|
||||
columnName,
|
||||
}: {
|
||||
fieldMetadataId: string;
|
||||
fieldMetadata: FieldMetadataEntity;
|
||||
columnName: string;
|
||||
}) => {
|
||||
fieldMetadataToColumnNamesMap.set(fieldMetadataId, [columnName]);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
processFieldMetadataForColumnNameMapping(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
processor,
|
||||
);
|
||||
|
||||
return fieldMetadataToColumnNamesMap;
|
||||
}
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
|
||||
import { FieldMetadataRelationSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils';
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
|
||||
export type ColumnNameProcessor = {
|
||||
processCompositeField: ({
|
||||
fieldMetadataId,
|
||||
fieldMetadata,
|
||||
compositeType,
|
||||
}: {
|
||||
fieldMetadataId: string;
|
||||
fieldMetadata: FieldMetadataEntity;
|
||||
compositeType: CompositeType;
|
||||
}) => void;
|
||||
processRelationField: ({
|
||||
fieldMetadataId,
|
||||
fieldMetadata,
|
||||
columnName,
|
||||
}: {
|
||||
fieldMetadataId: string;
|
||||
fieldMetadata: FieldMetadataEntity;
|
||||
columnName: string;
|
||||
}) => void;
|
||||
processSimpleField: ({
|
||||
fieldMetadataId,
|
||||
fieldMetadata,
|
||||
columnName,
|
||||
}: {
|
||||
fieldMetadataId: string;
|
||||
fieldMetadata: FieldMetadataEntity;
|
||||
columnName: string;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
export function processFieldMetadataForColumnNameMapping(
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
||||
processor: ColumnNameProcessor,
|
||||
) {
|
||||
for (const [fieldMetadataId, fieldMetadata] of Object.entries(
|
||||
objectMetadataItemWithFieldMaps.fieldsById,
|
||||
)) {
|
||||
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
|
||||
|
||||
if (!compositeType) {
|
||||
throw new PermissionsException(
|
||||
`Composite type not found for field metadata type ${fieldMetadata.type}`,
|
||||
PermissionsExceptionCode.COMPOSITE_TYPE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
processor.processCompositeField({
|
||||
fieldMetadataId,
|
||||
fieldMetadata,
|
||||
compositeType,
|
||||
});
|
||||
} else {
|
||||
if (isFieldMetadataRelationOrMorphRelation(fieldMetadata)) {
|
||||
const fieldMetadataSettings =
|
||||
fieldMetadata.settings as FieldMetadataRelationSettings;
|
||||
|
||||
if (fieldMetadataSettings?.relationType === RelationType.ONE_TO_MANY) {
|
||||
continue;
|
||||
}
|
||||
const columnName = fieldMetadataSettings?.joinColumnName;
|
||||
|
||||
if (!columnName) {
|
||||
throw new PermissionsException(
|
||||
`Join column name is required for relation field metadata ${fieldMetadata.name}`,
|
||||
PermissionsExceptionCode.JOIN_COLUMN_NAME_REQUIRED,
|
||||
);
|
||||
}
|
||||
processor.processRelationField({
|
||||
fieldMetadataId,
|
||||
fieldMetadata,
|
||||
columnName,
|
||||
});
|
||||
} else {
|
||||
const columnName = computeColumnName(fieldMetadata);
|
||||
|
||||
processor.processSimpleField({
|
||||
fieldMetadataId,
|
||||
fieldMetadata,
|
||||
columnName,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user