Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fix] Prevent fields name conflicts with composite subfields names #6713

Merged
merged 11 commits into from
Aug 23, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/com
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';

export const FIELD_LINKS_MOCK_NAME = 'fieldLink';
export const FIELD_CURRENCY_MOCK_NAME = 'fieldCurrency';
export const FIELD_ADDRESS_MOCK_NAME = 'fieldAddress';
export const FIELD_ACTOR_MOCK_NAME = 'fieldActor';
export const FIELD_FULL_NAME_MOCK_NAME = 'fieldFullName';

export const fieldNumberMock = {
name: 'fieldNumber',
type: FieldMetadataType.NUMBER,
Expand All @@ -16,15 +22,8 @@ export const fieldTextMock = {
defaultValue: null,
};

export const fieldLinkMock = {
name: 'fieldLink',
type: FieldMetadataType.LINK,
isNullable: false,
defaultValue: { label: '', url: '' },
};

export const fieldCurrencyMock = {
name: 'fieldCurrency',
name: FIELD_CURRENCY_MOCK_NAME,
type: FieldMetadataType.CURRENCY,
isNullable: true,
defaultValue: { amountMicros: null, currencyCode: "''" },
Expand Down Expand Up @@ -89,7 +88,7 @@ export const fieldRelationMock = {
};

const fieldLinksMock = {
name: 'fieldLinks',
name: FIELD_LINKS_MOCK_NAME,
type: FieldMetadataType.LINKS,
isNullable: false,
defaultValue: [
Expand Down Expand Up @@ -147,7 +146,7 @@ const fieldNumericMock = {
};

const fieldFullNameMock = {
name: 'fieldFullName',
name: FIELD_FULL_NAME_MOCK_NAME,
type: FieldMetadataType.FULL_NAME,
isNullable: true,
defaultValue: { firstName: '', lastName: '' },
Expand All @@ -168,7 +167,7 @@ const fieldPositionMock = {
};

const fieldAddressMock = {
name: 'fieldAddress',
name: FIELD_ADDRESS_MOCK_NAME,
type: FieldMetadataType.ADDRESS,
isNullable: true,
defaultValue: {
Expand Down Expand Up @@ -198,7 +197,7 @@ const fieldRichTextMock = {
};

const fieldActorMock = {
name: 'fieldActor',
name: FIELD_ACTOR_MOCK_NAME,
type: FieldMetadataType.ACTOR,
isNullable: true,
defaultValue: {
Expand All @@ -217,7 +216,6 @@ export const fields = [
fieldBooleanMock,
fieldNumberMock,
fieldNumericMock,
fieldLinkMock,
fieldLinksMock,
fieldCurrencyMock,
fieldFullNameMock,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';

import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';

import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { 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';

Expand Down Expand Up @@ -52,7 +52,7 @@ export class ArgsAliasFactory {
isCompositeFieldMetadataType(fieldMetadata.type)
) {
// Get composite type definition
const compositeType = compositeTypeDefintions.get(fieldMetadata.type);
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);

if (!compositeType) {
this.logger.error(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { Injectable, Logger } from '@nestjs/common';

import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';

import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { createCompositeFieldKey } from 'src/engine/api/graphql/workspace-query-builder/utils/composite-field-metadata.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';

@Injectable()
export class FieldAliasFactory {
Expand All @@ -23,7 +23,7 @@ export class FieldAliasFactory {
}

// If it's a composite field, we need to get the definition
const compositeType = compositeTypeDefintions.get(fieldMetadata.type);
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);

if (!compositeType) {
this.logger.error(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import { Injectable, Logger } from '@nestjs/common';

import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';

import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory';
import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { CompositeObjectTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory';
import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory';
import { CompositeEnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-enum-type-definition.factory';
import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory';
import { CompositeObjectTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory';
import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';

import { TypeDefinitionsStorage } from './storages/type-definitions.storage';
import {
ObjectTypeDefinitionFactory,
ObjectTypeDefinitionKind,
} from './factories/object-type-definition.factory';
import { ConnectionTypeDefinitionFactory } from './factories/connection-type-definition.factory';
import { EdgeTypeDefinitionFactory } from './factories/edge-type-definition.factory';
import { ExtendObjectTypeDefinitionFactory } from './factories/extend-object-type-definition.factory';
import {
InputTypeDefinitionFactory,
InputTypeDefinitionKind,
} from './factories/input-type-definition.factory';
import {
ObjectTypeDefinitionFactory,
ObjectTypeDefinitionKind,
} from './factories/object-type-definition.factory';
import { WorkspaceBuildSchemaOptions } from './interfaces/workspace-build-schema-optionts.interface';
import { ConnectionTypeDefinitionFactory } from './factories/connection-type-definition.factory';
import { EdgeTypeDefinitionFactory } from './factories/edge-type-definition.factory';
import { ExtendObjectTypeDefinitionFactory } from './factories/extend-object-type-definition.factory';
import { TypeDefinitionsStorage } from './storages/type-definitions.storage';
import { objectContainsRelationField } from './utils/object-contains-relation-field';

@Injectable()
Expand Down Expand Up @@ -55,7 +55,7 @@ export class TypeDefinitionsGenerator {
* GENERATE COMPOSITE TYPE OBJECTS
*/
private generateCompositeTypeDefs(options: WorkspaceBuildSchemaOptions) {
const compositeTypeCollection = [...compositeTypeDefintions.values()];
const compositeTypeCollection = [...compositeTypeDefinitions.values()];

this.logger.log(
`Generating composite type objects: [${compositeTypeCollection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { BadRequestException } from '@nestjs/common';

import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';

import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';

Expand All @@ -13,7 +13,7 @@ export const checkFields = (
const fieldMetadataNames = objectMetadata.fields
.map((field) => {
if (isCompositeFieldMetadataType(field.type)) {
const compositeType = compositeTypeDefintions.get(field.type);
const compositeType = compositeTypeDefinitions.get(field.type);

if (!compositeType) {
throw new BadRequestException(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { BadRequestException } from '@nestjs/common';

import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { Record } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';

import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';

Expand All @@ -14,7 +14,7 @@ export const checkArrayFields = (
const fieldMetadataNames = objectMetadata.fields
.map((field) => {
if (isCompositeFieldMetadataType(field.type)) {
const compositeType = compositeTypeDefintions.get(field.type);
const compositeType = compositeTypeDefinitions.get(field.type);

if (!compositeType) {
throw new BadRequestException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
computeOrderByParameters,
computeStartingAfterParameters,
} from 'src/engine/core-modules/open-api/utils/parameters.utils';
import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { capitalize } from 'src/utils/capitalize';
Expand Down Expand Up @@ -74,7 +74,7 @@ const getSchemaComponentsProperties = (
case FieldMetadataType.ACTOR:
itemProperty = {
type: 'object',
properties: compositeTypeDefintions
properties: compositeTypeDefinitions
.get(field.type)
?.properties?.reduce((properties, property) => {
properties[property.name] = getFieldProperties(property.type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export type CompositeFieldsDefinitionFunction = (
fieldMetadata?: FieldMetadataInterface,
) => FieldMetadataInterface[];

export const compositeTypeDefintions = new Map<
export const compositeTypeDefinitions = new Map<
FieldMetadataType,
CompositeType
>([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { v4 as uuidV4 } from 'uuid';

import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input';
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
Expand All @@ -28,6 +28,7 @@ import {
} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { generateNullable } from 'src/engine/metadata-modules/field-metadata/utils/generate-nullable';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import {
Expand All @@ -40,6 +41,10 @@ import {
NameTooLongException,
validateMetadataNameOrThrow,
} from 'src/engine/metadata-modules/utils/validate-metadata-name.utils';
import {
NameNotAvailableException,
validateNameAvailabilityOrThrow,
} from 'src/engine/metadata-modules/utils/validate-name-availability.utils';
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.service';
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
import {
Expand Down Expand Up @@ -140,7 +145,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
);
}

this.validateFieldMetadataInput<CreateFieldInput>(fieldMetadataInput);
this.validateFieldMetadataInput<CreateFieldInput>(
fieldMetadataInput,
objectMetadata,
);

const fieldAlreadyExists = await fieldMetadataRepository.findOne({
where: {
Expand Down Expand Up @@ -355,7 +363,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
}
}

this.validateFieldMetadataInput<UpdateFieldInput>(fieldMetadataInput);
this.validateFieldMetadataInput<UpdateFieldInput>(
fieldMetadataInput,
objectMetadata,
);

const updatableFieldInput =
existingFieldMetadata.isCustom === false
Expand Down Expand Up @@ -485,7 +496,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
await fieldMetadataRepository.delete(fieldMetadata.id);

if (isCompositeFieldMetadataType(fieldMetadata.type)) {
const compositeType = compositeTypeDefintions.get(fieldMetadata.type);
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);

if (!compositeType) {
throw new Error(
Expand Down Expand Up @@ -673,10 +684,14 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit

private validateFieldMetadataInput<
T extends UpdateFieldInput | CreateFieldInput,
>(fieldMetadataInput: T): T {
>(fieldMetadataInput: T, objectMetadata: ObjectMetadataEntity): T {
if (fieldMetadataInput.name) {
try {
validateMetadataNameOrThrow(fieldMetadataInput.name);
validateNameAvailabilityOrThrow(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure to completely understand this validateMetadataNameOrThrow vs validateNameAvailabilityOrThrow from the outside
I feel they should be wrapped together
maybe validateMetadataNameOrThrow should call validateNameAvailabilityOrThrow internally?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the functions names to add clarity, but one cannot call another as validateMetadataNameOrThrow (renamed to validateMetadataNameValidityOrThrow) checks for the correctness of the string (only contains non-latin char, does not start with digit etc) and is used in other services to check all metadata names (for objects and relations too)

fieldMetadataInput.name,
objectMetadata,
);
} catch (error) {
if (error instanceof InvalidStringException) {
throw new FieldMetadataException(
Expand All @@ -688,6 +703,11 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
`Name "${fieldMetadataInput.name}" exceeds 63 characters`,
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
);
} else if (error instanceof NameNotAvailableException) {
throw new FieldMetadataException(
`Name "${fieldMetadataInput.name}" is not available`,
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
);
} else {
throw error;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
FIELD_ACTOR_MOCK_NAME,
FIELD_ADDRESS_MOCK_NAME,
FIELD_CURRENCY_MOCK_NAME,
FIELD_FULL_NAME_MOCK_NAME,
FIELD_LINKS_MOCK_NAME,
objectMetadataItemMock,
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
import {
NameNotAvailableException,
validateNameAvailabilityOrThrow,
} from 'src/engine/metadata-modules/utils/validate-name-availability.utils';

describe('validateNameAvailabilityOrThrow', () => {
const objectMetadata = objectMetadataItemMock;

it('does not throw if name is not reserved', () => {
const name = 'testName';

expect(() =>
validateNameAvailabilityOrThrow(name, objectMetadata),
).not.toThrow();
});

describe('error cases', () => {
it('throws error with LINKS suffixes', () => {
const name = `${FIELD_LINKS_MOCK_NAME}PrimaryLinkLabel`;

expect(() =>
validateNameAvailabilityOrThrow(name, objectMetadata),
).toThrow(NameNotAvailableException);
});
it('throws error with CURRENCY suffixes', () => {
const name = `${FIELD_CURRENCY_MOCK_NAME}AmountMicros`;

expect(() =>
validateNameAvailabilityOrThrow(name, objectMetadata),
).toThrow(NameNotAvailableException);
});
it('throws error with FULL_NAME suffixes', () => {
const name = `${FIELD_FULL_NAME_MOCK_NAME}FirstName`;

expect(() =>
validateNameAvailabilityOrThrow(name, objectMetadata),
).toThrow(NameNotAvailableException);
});
it('throws error with ACTOR suffixes', () => {
const name = `${FIELD_ACTOR_MOCK_NAME}Name`;

expect(() =>
validateNameAvailabilityOrThrow(name, objectMetadata),
).toThrow(NameNotAvailableException);
});
it('throws error with ADDRESS suffixes', () => {
const name = `${FIELD_ADDRESS_MOCK_NAME}AddressStreet1`;

expect(() =>
validateNameAvailabilityOrThrow(name, objectMetadata),
).toThrow(NameNotAvailableException);
});
});
});
Loading
Loading