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] Support non latin characters in schema names #5063

Merged
merged 13 commits into from
Apr 23, 2024
Merged

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import styled from '@emotion/styled';

import { validateMetadataLabel } from '@/object-metadata/utils/validateMetadataLabel';
import { H2Title } from '@/ui/display/typography/components/H2Title';
import { IconPicker } from '@/ui/input/components/IconPicker';
import { TextArea } from '@/ui/input/components/TextArea';
Expand Down Expand Up @@ -51,9 +50,7 @@ export const SettingsObjectFieldFormSection = ({
placeholder="Employees"
value={name}
onChange={(value) => {
if (!value || validateMetadataLabel(value)) {
onChange?.({ label: value });
}
onChange?.({ label: value });
}}
disabled={disabled}
fullWidth
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { useIcons } from 'twenty-ui';

import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation';
import { validateMetadataLabel } from '@/object-metadata/utils/validateMetadataLabel';
import { IconPicker } from '@/ui/input/components/IconPicker';
import { Select } from '@/ui/input/components/Select';
import { TextInput } from '@/ui/input/components/TextInput';
Expand Down Expand Up @@ -121,11 +120,9 @@ export const SettingsObjectFieldRelationForm = ({
placeholder="Field name"
value={values.field.label}
onChange={(value) => {
if (!value || validateMetadataLabel(value)) {
onChange({
field: { ...values.field, label: value },
});
}
onChange({
field: { ...values.field, label: value },
});
}}
fullWidth
/>
Expand Down
1 change: 1 addition & 0 deletions packages/twenty-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"lodash.uniqby": "^4.7.0",
"passport": "^0.7.0",
"psl": "^1.9.0",
"transliteration": "^2.3.5",
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

"tsconfig-paths": "^4.2.0"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { BadRequestException } from '@nestjs/common';

export class ChararactersNotSupportedException extends BadRequestException {
constructor(string: string) {
const message = `String "${string}" contains unsupported characters`;

super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import {
import { DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input';
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import { formatString } from 'src/engine/metadata-modules/utils/format-string.util';
import { ChararactersNotSupportedException } from 'src/engine/metadata-modules/errors/CharactersNotSupportedException';

import {
FieldMetadataEntity,
Expand Down Expand Up @@ -114,6 +116,9 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
fieldMetadataInput.options = generateRatingOptions();
}

fieldMetadataInput =
this.formatFieldMetadataInput<CreateFieldInput>(fieldMetadataInput);

const fieldAlreadyExists = await fieldMetadataRepository.findOne({
where: {
name: fieldMetadataInput.name,
Expand Down Expand Up @@ -293,6 +298,9 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
}
}

fieldMetadataInput =
this.formatFieldMetadataInput<UpdateFieldInput>(fieldMetadataInput);

const updatableFieldInput =
existingFieldMetadata.isCustom === false
? this.buildUpdatableStandardFieldInput(
Expand Down Expand Up @@ -533,4 +541,28 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
};
}
}

private formatFieldMetadataInput<
T extends UpdateFieldInput | CreateFieldInput,
>(fieldMetadataInput: T): T {
if (fieldMetadataInput.name) {
try {
return (fieldMetadataInput = {
...fieldMetadataInput,
name: formatString(fieldMetadataInput.name),
});
} catch (error) {
if (error instanceof ChararactersNotSupportedException) {
console.error(error.message);
ijreilly marked this conversation as resolved.
Show resolved Hide resolved
throw new BadRequestException(
`Characters used in name "${fieldMetadataInput.name}" are not supported`,
);
} else {
throw error;
}
}
}

return fieldMetadataInput;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/
import { UpdateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { formatObjectMetadataInput } from 'src/engine/metadata-modules/object-metadata/utils/format-object-metadata-input.util';

@Injectable()
export class BeforeUpdateOneObject<T extends UpdateObjectInput>
Expand Down Expand Up @@ -100,6 +101,8 @@ export class BeforeUpdateOneObject<T extends UpdateObjectInput>
}
}

instance.update = formatObjectMetadataInput<T>(instance.update);

this.checkIfFieldIsEditable(instance.update, objectMetadata);

return instance;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
FeatureFlagKeys,
} from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { formatObjectMetadataInput } from 'src/engine/metadata-modules/object-metadata/utils/format-object-metadata-input.util';

import { ObjectMetadataEntity } from './object-metadata.entity';

Expand Down Expand Up @@ -237,6 +238,8 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
objectMetadataInput.workspaceId,
);

objectMetadataInput = formatObjectMetadataInput(objectMetadataInput);

if (
objectMetadataInput.nameSingular.toLowerCase() ===
objectMetadataInput.namePlural.toLowerCase()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { BadRequestException } from '@nestjs/common';

import { ChararactersNotSupportedException } from 'src/engine/metadata-modules/errors/CharactersNotSupportedException';
import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
import { UpdateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
import { formatString } from 'src/engine/metadata-modules/utils/format-string.util';

export const formatObjectMetadataInput = <
T extends UpdateObjectInput | CreateObjectInput,
>(
objectMetadataInput: T,
): T => {
try {
return (objectMetadataInput = {
...objectMetadataInput,
nameSingular: objectMetadataInput.nameSingular
? formatString(objectMetadataInput.nameSingular)
: objectMetadataInput.nameSingular,
namePlural: objectMetadataInput.namePlural
? formatString(objectMetadataInput.namePlural)
: objectMetadataInput.namePlural,
});
} catch (error) {
if (error instanceof ChararactersNotSupportedException) {
console.error(error.message);
throw new BadRequestException(
`Characters used in name "${objectMetadataInput.nameSingular}" or "${objectMetadataInput.namePlural}" are not supported`,
);
} else {
throw error;
}
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
import { formatString } from 'src/engine/metadata-modules/utils/format-string.util';
import { ChararactersNotSupportedException } from 'src/engine/metadata-modules/errors/CharactersNotSupportedException';

import {
RelationMetadataEntity,
Expand Down Expand Up @@ -51,6 +53,23 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
relationMetadataInput,
);

try {
relationMetadataInput = {
...relationMetadataInput,
fromName: formatString(relationMetadataInput.fromName),
toName: formatString(relationMetadataInput.toName),
};
} catch (error) {
if (error instanceof ChararactersNotSupportedException) {
console.error(error.message);
throw new BadRequestException(
`Characters used in name "${relationMetadataInput.fromName}" or "${relationMetadataInput.toName}" are not supported`,
);
} else {
throw error;
}
}

await this.validateCreateRelationMetadataInput(
relationMetadataInput,
objectMetadataMap,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
formatString,
validPattern,
} from 'src/engine/metadata-modules/utils/format-string.util';

describe('formatString', () => {
it('format strings starting with digits', () => {
const input = '123string';
const expected = 'string123';

expect(formatString(input).match(validPattern)?.length).toBe(1);
expect(formatString(input)).toEqual(expected);

const inputCapitalized = '123String';
const expectedCamelCased = 'string123';

expect(formatString(inputCapitalized).match(validPattern)?.length).toBe(1);
expect(formatString(inputCapitalized)).toEqual(expectedCamelCased);
});

it('format strings with non latin characters', () => {
const input = 'בְרִבְרִ';
const expected = 'bRibRiTransliterated';

expect(formatString(input).match(validPattern)?.length).toBe(1);
expect(formatString(input)).toEqual(expected);
});

it('format strings with mixed characters', () => {
const input = '2aaבְרִבְרִ';
const expected = 'aabRibRi2Transliterated';

expect(formatString(input).match(validPattern)?.length).toBe(1);
expect(formatString(input)).toEqual(expected);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { transliterate, slugify } from 'transliteration';
import toCamelCase from 'lodash.camelcase';

import { ChararactersNotSupportedException } from 'src/engine/metadata-modules/errors/CharactersNotSupportedException';

export const validPattern = /^[a-zA-Z][a-zA-Z0-9 ]*$/;
const startsWithDigitPattern = /^[^\d]*(\d+)/;

export const formatString = (string: string): string => {
let formattedString = string;

if (formattedString.match(validPattern)) {
return formattedString;
}

if (formattedString.match(startsWithDigitPattern)) {
const digitsAtStart = formattedString.match(startsWithDigitPattern)?.[0];

formattedString =
formattedString.slice(digitsAtStart?.length || 0) + digitsAtStart;
}

if (formattedString.match(validPattern)) {
return toCamelCase(formattedString);
}

formattedString = toCamelCase(
slugify(transliterate(formattedString, { trim: true }) + '-transliterated'),
);

if (!formattedString.match(validPattern)) {
throw new ChararactersNotSupportedException(string);
}

return formattedString;
};
15 changes: 14 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -45804,6 +45804,18 @@ __metadata:
languageName: node
linkType: hard

"transliteration@npm:^2.3.5":
version: 2.3.5
resolution: "transliteration@npm:2.3.5"
dependencies:
yargs: "npm:^17.5.1"
bin:
slugify: dist/bin/slugify
transliterate: dist/bin/transliterate
checksum: 68397225c2ca59b8e33206c65f905724e86b64460cbf90576d352dc2366e763ded97e2c7b8b1f140fb36a565d61a97c51080df9fa638e6b1769f6cb24f383756
languageName: node
linkType: hard

"traverse@npm:0.6.7":
version: 0.6.7
resolution: "traverse@npm:0.6.7"
Expand Down Expand Up @@ -46300,6 +46312,7 @@ __metadata:
passport: "npm:^0.7.0"
psl: "npm:^1.9.0"
rimraf: "npm:^5.0.5"
transliteration: "npm:^2.3.5"
tsconfig-paths: "npm:^4.2.0"
typescript: "npm:5.3.3"
languageName: unknown
Expand Down Expand Up @@ -49452,7 +49465,7 @@ __metadata:
languageName: node
linkType: hard

"yargs@npm:^17.0.0, yargs@npm:^17.3.1, yargs@npm:^17.6.2, yargs@npm:^17.7.2":
"yargs@npm:^17.0.0, yargs@npm:^17.3.1, yargs@npm:^17.5.1, yargs@npm:^17.6.2, yargs@npm:^17.7.2":
version: 17.7.2
resolution: "yargs@npm:17.7.2"
dependencies:
Expand Down
Loading