Compare commits

...

7 Commits

Author SHA1 Message Date
Weiko a8f0c90add fix server linter 2025-06-26 15:54:28 +02:00
Weiko 10eba9e36d Add fix schema array type command (#12887)
Following this fix https://github.com/twentyhq/twenty/pull/12874 we now
need to add a command to retroactively fix existing columns

---------

Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com>
Co-authored-by: prastoin <paul@twenty.com>
2025-06-26 15:54:18 +02:00
Weiko 4b1fb10e53 Fix upgrade command updating version during dry run (#12909)
Fix upgrade command updating version during dry run
2025-06-26 15:54:06 +02:00
Paul Rastoin 6061184361 fix(server): incr metadata version (#12907)
Following https://github.com/twentyhq/twenty/pull/12883
2025-06-26 15:53:58 +02:00
Guillim 1b457cb515 escaping special chars for events (#12872)
Escaping newly discovered special chars for events

- \x00 is a Unicode for nul
- \x7f is a Unicode for delete

Edit:
i initially, just in case after looking at the logs, i removed the
single quotes as well (there are fobiden in the standard RFC 5545) but
after reflexion other props than icalUID rely on this sanitization so
leaving as such. It should already be taken care of anyway by typeorm


Ref sentry :
https://twenty-v7.sentry.io/issues/6567295627/?environment=prod&environment=prod-eu&project=4507072499810304&query=&referrer=issue-stream&stream_index=13

Fixes https://github.com/twentyhq/twenty/issues/12827
2025-06-26 15:53:48 +02:00
Paul Rastoin 0e8a6f73e3 Fix standard object computed metadata (#12883)
# Introduction
close https://github.com/twentyhq/twenty/issues/12879

This PR has a global impact all on workspaces
It should be crash tested in local using an anon extract of the db
2025-06-26 15:53:40 +02:00
Weiko 9f04ccb94a Fix array field migration (#12874)
Fixes https://github.com/twentyhq/twenty/issues/12726

## Context
Regression introduced in https://github.com/twentyhq/twenty/pull/12639
We now run raw queries for some migrations (column creations for
example) and we created a `typeormBuildCreateColumnSql` util for that.
The issue is that previously we were using typeorm methods which was
using isArray from the input to create $type[] (text[], number[])
properly which was not done in the new `typeormBuildCreateColumnSql`
util (so the type was text, number, etc...)
Edit: actually this was correctly implemented for Enum types (multi
select fields) but not Array type, I've updated the code accordingly
2025-06-25 17:05:55 +02:00
16 changed files with 407 additions and 59 deletions
@@ -114,10 +114,13 @@ export abstract class UpgradeCommandRunner extends ActiveOrSuspendedWorkspacesMi
await this.syncWorkspaceMetadataCommand.runOnWorkspace(args);
await this.runAfterSyncMetadata(args);
await this.workspaceRepository.update(
{ id: workspaceId },
{ version: this.currentAppVersion.version },
);
if (!options.dryRun) {
await this.workspaceRepository.update(
{ id: workspaceId },
{ version: this.currentAppVersion.version },
);
}
this.logger.log(
chalk.blue(`Upgrade for workspace ${workspaceId} completed.`),
);
@@ -0,0 +1,102 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { FieldMetadataType } from 'twenty-shared/types';
import { Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
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 { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { DatabaseStructureService } from 'src/engine/workspace-manager/workspace-health/services/database-structure.service';
@Command({
name: 'upgrade:1-1:fix-schema-array-type',
description: 'Fix columns for ARRAY fields to be text[] in the DB schema',
})
export class FixSchemaArrayTypeCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly databaseStructureService: DatabaseStructureService,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly typeORMService: TypeORMService,
@InjectRepository(FieldMetadataEntity, 'core')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(`Fixing ARRAY field columns for workspace ${workspaceId}`);
const arrayFields: FieldMetadataEntity[] =
await this.fieldMetadataRepository.find({
where: {
workspaceId,
type: FieldMetadataType.ARRAY,
isCustom: true,
},
relations: ['object'],
});
if (arrayFields.length === 0) {
this.logger.log('No ARRAY fields found for this workspace.');
return;
}
for (const field of arrayFields) {
const object = field.object;
const tableName = computeObjectTargetTable(object);
const schemaName =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const columns =
await this.databaseStructureService.getWorkspaceTableColumns(
schemaName,
tableName,
);
const columnName = computeColumnName(field);
const dbColumn = columns.find((col) => col.columnName === columnName);
if (!dbColumn) {
this.logger.warn(
`Column ${columnName} not found in table ${schemaName}.${tableName}`,
);
continue;
}
if (dbColumn.dataType === 'text[]' && dbColumn.isArray) {
continue;
}
this.logger.log(
`Altering column ${schemaName}.${tableName}.${columnName} to type text[] (was ${dbColumn.dataType})`,
);
if (!options.dryRun) {
const queryRunner = this.typeORMService
.getMainDataSource()
.createQueryRunner();
await queryRunner.connect();
try {
await queryRunner.query(
`ALTER TABLE "${schemaName}"."${tableName}" ALTER COLUMN "${columnName}" TYPE text[] USING "${columnName}"::text[];`,
);
} finally {
await queryRunner.release();
}
}
}
}
}
@@ -0,0 +1,74 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { IsNull, Not, Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@Command({
name: 'upgrade:1-1:fix-update-standard-fields-is-label-synced-with-name',
description:
'Fix isLabelSyncedWithName property for standard fields to match actual label-name synchronization state',
})
export class FixUpdateStandardFieldsIsLabelSyncedWithName extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectRepository(FieldMetadataEntity, 'core')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(`Updating standard fields for workspace ${workspaceId}`);
const workspaceStandardFields = await this.fieldMetadataRepository.find({
where: {
workspaceId,
isCustom: false,
standardId: Not(IsNull()),
},
});
let updatedFields = 0;
for (const field of workspaceStandardFields) {
const isLabelSyncedWithName =
computeMetadataNameFromLabel(field.label) === field.name;
if (field.isLabelSyncedWithName === isLabelSyncedWithName) {
continue;
}
if (!options.dryRun) {
await this.fieldMetadataRepository.update(field.id, {
isLabelSyncedWithName,
});
}
updatedFields++;
this.logger.log(`Updated isLabelSyncedMetadata for field ${field.id}`);
}
if (!options.dryRun && updatedFields > 0) {
await this.workspaceMetadataVersionService.incrementMetadataVersion(
workspaceId,
);
}
this.logger.log(
`Updated ${updatedFields} field.s for workspace ${workspaceId}`,
);
}
}
@@ -0,0 +1,46 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FixSchemaArrayTypeCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-fix-schema-array-type.command';
import { FixUpdateStandardFieldsIsLabelSyncedWithName } from 'src/database/commands/upgrade-version-command/1-1/1-1-fix-update-standard-field-is-label-synced-with-name.command';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { WorkspaceHealthModule } from 'src/engine/workspace-manager/workspace-health/workspace-health.module';
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
@Module({
imports: [
TypeOrmModule.forFeature(
[
Workspace,
AppToken,
User,
UserWorkspace,
FieldMetadataEntity,
ObjectMetadataEntity,
],
'core',
),
WorkspaceDataSourceModule,
WorkspaceMigrationRunnerModule,
WorkspaceMetadataVersionModule,
WorkspaceHealthModule,
TypeORMModule,
],
providers: [
FixUpdateStandardFieldsIsLabelSyncedWithName,
FixSchemaArrayTypeCommand,
],
exports: [
FixUpdateStandardFieldsIsLabelSyncedWithName,
FixSchemaArrayTypeCommand,
],
})
export class V1_1_UpgradeVersionCommandModule {}
@@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { V0_54_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/0-54/0-54-upgrade-version-command.module';
import { V0_55_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/0-55/0-55-upgrade-version-command.module';
import { V1_1_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-1/1-1-upgrade-version-command.module';
import {
DatabaseMigrationService,
UpgradeCommand,
@@ -15,6 +16,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
TypeOrmModule.forFeature([Workspace], 'core'),
V0_54_UpgradeVersionCommandModule,
V0_55_UpgradeVersionCommandModule,
V1_1_UpgradeVersionCommandModule,
WorkspaceSyncMetadataModule,
],
providers: [DatabaseMigrationService, UpgradeCommand],
@@ -21,6 +21,8 @@ import { FixStandardSelectFieldsPositionCommand } from 'src/database/commands/up
import { LowercaseUserAndInvitationEmailsCommand } from 'src/database/commands/upgrade-version-command/0-54/0-54-lowercase-user-and-invitation-emails.command';
import { MigrateDefaultAvatarUrlToUserWorkspaceCommand } from 'src/database/commands/upgrade-version-command/0-54/0-54-migrate-default-avatar-url-to-user-workspace.command';
import { DeduplicateIndexedFieldsCommand } from 'src/database/commands/upgrade-version-command/0-55/0-55-deduplicate-indexed-fields.command';
import { FixSchemaArrayTypeCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-fix-schema-array-type.command';
import { FixUpdateStandardFieldsIsLabelSyncedWithName } from 'src/database/commands/upgrade-version-command/1-1/1-1-fix-update-standard-field-is-label-synced-with-name.command';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@@ -135,6 +137,10 @@ export class UpgradeCommand extends UpgradeCommandRunner {
// 0.55 Commands
protected readonly deduplicateIndexedFieldsCommand: DeduplicateIndexedFieldsCommand,
// 1.1 Commands
protected readonly fixSchemaArrayTypeCommand: FixSchemaArrayTypeCommand,
protected readonly fixUpdateStandardFieldsIsLabelSyncedWithNameCommand: FixUpdateStandardFieldsIsLabelSyncedWithName,
) {
super(
workspaceRepository,
@@ -175,12 +181,21 @@ export class UpgradeCommand extends UpgradeCommandRunner {
beforeSyncMetadata: [],
};
const commands_110: VersionCommands = {
beforeSyncMetadata: [
this.fixUpdateStandardFieldsIsLabelSyncedWithNameCommand,
this.fixSchemaArrayTypeCommand,
],
afterSyncMetadata: [],
};
this.allCommands = {
'0.53.0': commands_053,
'0.54.0': commands_054,
'0.55.0': commands_055,
'0.60.0': commands_060,
'1.0.0': commands_100,
'1.1.0': commands_110,
};
}
@@ -6,7 +6,7 @@ import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { generateDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/generate-default-value';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
import { TypedReflect } from 'src/utils/typed-reflect';
@@ -19,12 +19,8 @@ export interface WorkspaceFieldOptions<
> {
standardId: string;
type: T;
label:
| MessageDescriptor
| ((objectMetadata: ObjectMetadataEntity) => MessageDescriptor);
description?:
| MessageDescriptor
| ((objectMetadata: ObjectMetadataEntity) => MessageDescriptor);
label: MessageDescriptor;
description?: MessageDescriptor;
icon?: string;
defaultValue?: FieldMetadataDefaultValue<T>;
options?: FieldMetadataOptions<T>;
@@ -76,30 +72,18 @@ export function WorkspaceField<T extends FieldMetadataType>(
const defaultValue = (options.defaultValue ??
generateDefaultValue(options.type)) as FieldMetadataDefaultValue | null;
const name = propertyKey.toString();
const label = options.label.message ?? '';
const isLabelSyncedWithName = computeMetadataNameFromLabel(label) === name;
metadataArgsStorage.addFields({
target: object.constructor,
standardId: options.standardId,
name: propertyKey.toString(),
label:
typeof options.label === 'function'
? (objectMetadata: ObjectMetadataEntity) =>
(
options.label as (
obj: ObjectMetadataEntity,
) => MessageDescriptor
)(objectMetadata).message ?? ''
: (options.label.message ?? ''),
name,
label,
type: options.type,
description:
typeof options.description === 'function'
? (objectMetadata: ObjectMetadataEntity) =>
(
options.description as (
obj: ObjectMetadataEntity,
) => MessageDescriptor
)(objectMetadata).message ?? ''
: (options.description?.message ?? ''),
isLabelSyncedWithName,
description: options.description?.message ?? '',
icon: options.icon,
defaultValue,
options: options.options,
@@ -5,14 +5,13 @@ import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metada
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
import { TypedReflect } from 'src/utils/typed-reflect';
interface WorkspaceRelationBaseOptions<TClass> {
standardId: string;
label:
| MessageDescriptor
| ((objectMetadata: ObjectMetadataEntity) => MessageDescriptor);
label: MessageDescriptor;
description?:
| MessageDescriptor
| ((objectMetadata: ObjectMetadataEntity) => MessageDescriptor);
@@ -64,20 +63,15 @@ export function WorkspaceRelation<TClass extends object>(
object,
propertyKey.toString(),
);
const name = propertyKey.toString();
const label = options.label.message ?? '';
const isLabelSyncedWithName = computeMetadataNameFromLabel(label) === name;
metadataArgsStorage.addRelations({
target: object.constructor,
standardId: options.standardId,
name: propertyKey.toString(),
label:
typeof options.label === 'function'
? (objectMetadata: ObjectMetadataEntity) =>
(
options.label as (
obj: ObjectMetadataEntity,
) => MessageDescriptor
)(objectMetadata).message ?? ''
: (options.label.message ?? ''),
name,
label,
type: options.type,
description:
typeof options.description === 'function'
@@ -96,6 +90,7 @@ export function WorkspaceRelation<TClass extends object>(
isNullable,
isSystem,
gate,
isLabelSyncedWithName,
});
};
}
@@ -105,4 +105,6 @@ export interface WorkspaceFieldMetadataArgs {
* Is active field.
*/
readonly asExpression?: string;
readonly isLabelSyncedWithName: boolean;
}
@@ -84,4 +84,6 @@ export interface WorkspaceRelationMetadataArgs {
* Is active field.
*/
readonly isActive?: boolean;
readonly isLabelSyncedWithName: boolean;
}
@@ -32,6 +32,6 @@ import { WorkspaceFixService } from './services/workspace-fix.service';
FieldMetadataHealthService,
WorkspaceFixService,
],
exports: [WorkspaceHealthService],
exports: [WorkspaceHealthService, DatabaseStructureService],
})
export class WorkspaceHealthModule {}
@@ -0,0 +1,95 @@
import { typeormBuildCreateColumnSql } from './typeorm-build-create-column-sql.util';
describe('typeormBuildCreateColumnSql', () => {
const table = { name: 'my_table' };
it('should generate SQL for a simple text column', () => {
const column = {
name: 'col1',
type: 'text',
isArray: false,
isNullable: false,
default: undefined,
generatedType: undefined,
asExpression: undefined,
};
const sql = typeormBuildCreateColumnSql({ table, column });
expect(sql).toBe('"col1" text NOT NULL');
});
it('should generate SQL for a text[] (array) column', () => {
const column = {
name: 'col2',
type: 'text',
isArray: true,
isNullable: true,
default: undefined,
generatedType: undefined,
asExpression: undefined,
};
const sql = typeormBuildCreateColumnSql({ table, column });
expect(sql).toBe('"col2" text[]');
});
it('should generate SQL for an enum column', () => {
const column = {
name: 'col3',
type: 'enum',
isArray: false,
isNullable: true,
default: undefined,
generatedType: undefined,
asExpression: undefined,
};
const sql = typeormBuildCreateColumnSql({ table, column });
expect(sql).toBe('"col3" "my_table_col3_enum"');
});
it('should generate SQL for an enum[] (array) column', () => {
const column = {
name: 'col4',
type: 'enum',
isArray: true,
isNullable: false,
default: undefined,
generatedType: undefined,
asExpression: undefined,
};
const sql = typeormBuildCreateColumnSql({ table, column });
expect(sql).toBe('"col4" "my_table_col4_enum"[] NOT NULL');
});
it('should include default and nullability', () => {
const column = {
name: 'col5',
type: 'text',
isArray: false,
isNullable: false,
default: "'default'",
generatedType: undefined,
asExpression: undefined,
};
const sql = typeormBuildCreateColumnSql({ table, column });
expect(sql).toBe('"col5" text NOT NULL DEFAULT \'default\'');
});
it('should include generatedType and asExpression', () => {
const column = {
name: 'col6',
type: 'text',
isArray: false,
isNullable: true,
default: undefined,
generatedType: 'STORED' as const,
asExpression: 'lower(col6)',
};
const sql = typeormBuildCreateColumnSql({ table, column });
expect(sql).toBe('"col6" text GENERATED ALWAYS AS (lower(col6)) STORED');
});
});
@@ -28,11 +28,12 @@ export const typeormBuildCreateColumnSql = ({
tableName: table.name,
columnName: column.name,
})}"`;
if (column.isArray) columnSql += ' array';
} else {
columnSql += ' ' + column.type;
}
if (column.isArray) columnSql += '[]';
if (column.generatedType === 'STORED' && column.asExpression) {
columnSql += ` GENERATED ALWAYS AS (${column.asExpression}) STORED`;
}
@@ -121,7 +121,7 @@ export class StandardFieldFactory {
* Create field metadata
*/
private createFieldMetadata(
workspaceEntityMetadataArgs: WorkspaceEntityMetadataArgs | undefined,
_workspaceEntityMetadataArgs: WorkspaceEntityMetadataArgs | undefined,
workspaceFieldMetadataArgs: WorkspaceFieldMetadataArgs,
context: WorkspaceSyncContext,
): PartialFieldMetadata[] {
@@ -153,7 +153,7 @@ export class StandardFieldFactory {
isActive: workspaceFieldMetadataArgs.isActive ?? true,
asExpression: workspaceFieldMetadataArgs.asExpression,
generatedType: workspaceFieldMetadataArgs.generatedType,
isLabelSyncedWithName: true,
isLabelSyncedWithName: workspaceFieldMetadataArgs.isLabelSyncedWithName,
},
];
}
@@ -192,7 +192,8 @@ export class StandardFieldFactory {
isNullable: true,
isUnique: false,
isActive: workspaceRelationMetadataArgs.isActive ?? true,
isLabelSyncedWithName: true,
isLabelSyncedWithName:
workspaceRelationMetadataArgs.isLabelSyncedWithName,
});
return fieldMetadataCollection;
@@ -1,4 +1,5 @@
import { calendar_v3 as calendarV3 } from 'googleapis';
import { EachTestingContext } from 'twenty-shared/testing';
import { formatGoogleCalendarEvents } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/format-google-calendar-event.util';
import { CalendarEventParticipantResponseStatus } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity';
@@ -77,23 +78,48 @@ describe('formatGoogleCalendarEvents', () => {
);
});
it('should sanitize a UCALID with improper exit char 0x00', () => {
const testCases: EachTestingContext<{ input: string; expected: string }>[] = [
{
title: 'should sanitize a UCALID with \u0000',
context: {
input: '\u0000eventStrange@google.com',
expected: 'eventStrange@google.com',
},
},
{
title: 'should sanitize a UCALID with \u0000',
context: {
input: '>\u0000\u0015-;_^W&p\u001f',
expected: '>\u0015-;_^W&p\u001f',
},
},
{
title: 'should sanitize a UCALID with \x00',
context: {
input: '\u0002y\u0013\x00',
expected: '\u0002y\u0013',
},
},
{
title: 'should sanitize a UCALID with del',
context: {
input: 'del\u0002y\u0013',
expected: 'del\u0002y\u0013',
},
},
];
it.each(testCases)('$title', ({ context }) => {
const mockGoogleEventWithImproperUcalid: calendarV3.Schema$Event = {
...mockGoogleEvent,
iCalUID: '\u0000eventStrange@google.com',
};
const mockGoogleEventWithImproperUcalid2: calendarV3.Schema$Event = {
...mockGoogleEvent,
iCalUID: '>\u0000\u0015-;_^W&p\u001f',
iCalUID: context.input,
};
const result = formatGoogleCalendarEvents([
mockGoogleEventWithImproperUcalid,
mockGoogleEventWithImproperUcalid2,
]);
expect(result[0].iCalUID).toBe('eventStrange@google.com');
expect(result[1].iCalUID).toBe('>\u0015-;_^W&p\u001f');
expect(result[0].iCalUID).toBe(context.expected);
});
});
@@ -23,5 +23,5 @@ export const sanitizeCalendarEvent = <T extends Record<string, any>>(
};
const sanitizeString = (value: string): string => {
return value.replace('\u0000', '');
return value.replace('\u0000', '').replace('\x00', '').replace('\x7f', '');
};