Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 27c9fd9d19 | |||
| a8f0c90add | |||
| 10eba9e36d | |||
| 4b1fb10e53 | |||
| 6061184361 | |||
| 1b457cb515 | |||
| 0e8a6f73e3 | |||
| 9f04ccb94a |
Vendored
+21
@@ -82,6 +82,27 @@
|
||||
"env": {
|
||||
"NODE_ENV": "test"
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "twenty-server - debug unit test file (to launch with test file open)",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeArgs": [
|
||||
"nx",
|
||||
"run",
|
||||
"twenty-server:jest",
|
||||
"--",
|
||||
"--config",
|
||||
"./jest.config.ts",
|
||||
"${relativeFile}"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/packages/twenty-server",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"env": {
|
||||
"NODE_ENV": "test"
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
+7
-4
@@ -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.`),
|
||||
);
|
||||
|
||||
+102
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+74
@@ -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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+46
@@ -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 {}
|
||||
+2
@@ -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],
|
||||
|
||||
+15
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`checkFields should accept valid relation field name 1`] = `"field 'pointOfContact' does not exist in 'opportunity' object"`;
|
||||
|
||||
exports[`checkFields should reject invalid field name 1`] = `"field 'wrongField' does not exist in 'opportunity' object"`;
|
||||
|
||||
exports[`checkFields should reject mix of valid and invalid field names 1`] = `"field 'wrongField' does not exist in 'opportunity' object"`;
|
||||
+55
-68
@@ -1,76 +1,63 @@
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
import { EachTestingContext } from 'twenty-shared/testing';
|
||||
|
||||
import {
|
||||
fieldNumberMock,
|
||||
objectMetadataItemMock,
|
||||
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
|
||||
import { OPPORTUNITY_WITH_FIELDS_MAPS } from 'src/engine/api/rest/core/query-builder/utils/__tests__/mocks/opportunity-field-maps.mock';
|
||||
import { checkFields } from 'src/engine/api/rest/core/query-builder/utils/check-fields.utils';
|
||||
import { checkArrayFields } from 'src/engine/api/rest/core/query-builder/utils/check-order-by.utils';
|
||||
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
|
||||
|
||||
describe('checkFields', () => {
|
||||
const completeFieldNumberMock: FieldMetadataInterface = {
|
||||
id: 'field-number-id',
|
||||
type: fieldNumberMock.type,
|
||||
name: fieldNumberMock.name,
|
||||
label: 'Field Number',
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
isNullable: fieldNumberMock.isNullable,
|
||||
defaultValue: fieldNumberMock.defaultValue,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const fieldsById: FieldMetadataMap = {
|
||||
'field-number-id': completeFieldNumberMock,
|
||||
};
|
||||
|
||||
const mockObjectMetadataWithFieldMaps = {
|
||||
...objectMetadataItemMock,
|
||||
fieldsById,
|
||||
fieldIdByName: {
|
||||
[completeFieldNumberMock.name]: completeFieldNumberMock.id,
|
||||
const testCases: EachTestingContext<{
|
||||
fields: string[];
|
||||
shouldThrow?: boolean;
|
||||
}>[] = [
|
||||
{
|
||||
title: 'should accept valid join column id',
|
||||
context: {
|
||||
fields: ['pointOfContactId'],
|
||||
},
|
||||
},
|
||||
fieldIdByJoinColumnName: {},
|
||||
indexMetadatas: [],
|
||||
};
|
||||
{
|
||||
title: 'should accept valid relation field name',
|
||||
context: {
|
||||
shouldThrow: true,
|
||||
fields: ['pointOfContact'],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should accept valid field name',
|
||||
context: {
|
||||
fields: ['position'],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should reject invalid field name',
|
||||
context: {
|
||||
fields: ['wrongField'],
|
||||
shouldThrow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should reject mix of valid and invalid field names',
|
||||
context: {
|
||||
fields: ['position', 'wrongField'],
|
||||
shouldThrow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should accept composite field',
|
||||
context: {
|
||||
fields: ['source'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it('should check field types', () => {
|
||||
expect(() =>
|
||||
checkFields(mockObjectMetadataWithFieldMaps, ['fieldNumber']),
|
||||
).not.toThrow();
|
||||
|
||||
expect(() =>
|
||||
checkFields(mockObjectMetadataWithFieldMaps, ['wrongField']),
|
||||
).toThrow();
|
||||
|
||||
expect(() =>
|
||||
checkFields(mockObjectMetadataWithFieldMaps, [
|
||||
'fieldNumber',
|
||||
'wrongField',
|
||||
]),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it('should check field types from array of fields', () => {
|
||||
expect(() =>
|
||||
checkArrayFields(mockObjectMetadataWithFieldMaps, [
|
||||
{ fieldNumber: undefined },
|
||||
]),
|
||||
).not.toThrow();
|
||||
|
||||
expect(() =>
|
||||
checkArrayFields(mockObjectMetadataWithFieldMaps, [
|
||||
{ wrongField: undefined },
|
||||
]),
|
||||
).toThrow();
|
||||
|
||||
expect(() =>
|
||||
checkArrayFields(mockObjectMetadataWithFieldMaps, [
|
||||
{ fieldNumber: undefined },
|
||||
{ wrongField: undefined },
|
||||
]),
|
||||
).toThrow();
|
||||
it.each(testCases)('$title', ({ context }) => {
|
||||
if (context.shouldThrow) {
|
||||
expect(() =>
|
||||
checkFields(OPPORTUNITY_WITH_FIELDS_MAPS, context.fields),
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
} else {
|
||||
expect(() =>
|
||||
checkFields(OPPORTUNITY_WITH_FIELDS_MAPS, context.fields),
|
||||
).not.toThrow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+463
@@ -0,0 +1,463 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
|
||||
export const OPPORTUNITY_WITH_FIELDS_MAPS = {
|
||||
id: '20202020-6e2c-42f6-a83c-cc58d776af88',
|
||||
nameSingular: 'opportunity',
|
||||
namePlural: 'opportunities',
|
||||
labelSingular: 'Opportunity',
|
||||
labelPlural: 'Opportunities',
|
||||
description: 'An opportunity',
|
||||
icon: 'IconTargetArrow',
|
||||
targetTableName: 'DEPRECATED',
|
||||
isCustom: false,
|
||||
isRemote: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isAuditLogged: true,
|
||||
isSearchable: true,
|
||||
labelIdentifierFieldMetadataId: '20202020-c2f1-4435-adca-22931f8b41b6',
|
||||
imageIdentifierFieldMetadataId: null,
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
indexMetadatas: [], // unused
|
||||
fieldsById: {
|
||||
'20202020-c2f1-4435-adca-22931f8b41b6': {
|
||||
id: '20202020-c2f1-4435-adca-22931f8b41b6',
|
||||
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
defaultValue: "''",
|
||||
description: 'The opportunity name',
|
||||
icon: 'IconTargetArrow',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: false,
|
||||
isUnique: false,
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
},
|
||||
'20202020-5eef-417a-b517-ebeedaa8e10b': {
|
||||
id: '20202020-5eef-417a-b517-ebeedaa8e10b',
|
||||
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
|
||||
type: FieldMetadataType.CURRENCY,
|
||||
name: 'amount',
|
||||
label: 'Amount',
|
||||
defaultValue: { amountMicros: null, currencyCode: "''" },
|
||||
description: 'Opportunity amount',
|
||||
icon: 'IconCurrencyDollar',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
},
|
||||
'20202020-597c-44d3-98ec-ea71aea5256b': {
|
||||
id: '20202020-597c-44d3-98ec-ea71aea5256b',
|
||||
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
name: 'closeDate',
|
||||
label: 'Close date',
|
||||
defaultValue: null,
|
||||
description: 'Opportunity close date',
|
||||
icon: 'IconCalendarEvent',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
},
|
||||
'20202020-9b94-454a-94ca-8afb09c68faf': {
|
||||
id: '20202020-9b94-454a-94ca-8afb09c68faf',
|
||||
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
|
||||
type: FieldMetadataType.SELECT,
|
||||
name: 'stage',
|
||||
label: 'Stage',
|
||||
defaultValue: "'NEW'",
|
||||
description: 'Opportunity stage',
|
||||
icon: 'IconProgressCheck',
|
||||
options: [
|
||||
{
|
||||
id: '20202020-dba8-4975-81bd-b29a41c0a387',
|
||||
color: 'red',
|
||||
label: 'New',
|
||||
value: 'NEW',
|
||||
position: 0,
|
||||
},
|
||||
{
|
||||
id: '20202020-1c9d-490c-940c-bf47addcd6a1',
|
||||
color: 'purple',
|
||||
label: 'Screening',
|
||||
value: 'SCREENING',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: '20202020-1368-438b-8702-6d5e5727a888',
|
||||
color: 'sky',
|
||||
label: 'Meeting',
|
||||
value: 'MEETING',
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
id: '20202020-41e4-4b4e-a038-f02645f55767',
|
||||
color: 'turquoise',
|
||||
label: 'Proposal',
|
||||
value: 'PROPOSAL',
|
||||
position: 3,
|
||||
},
|
||||
{
|
||||
id: '20202020-8acf-4934-8519-42f7c9133cd5',
|
||||
color: 'yellow',
|
||||
label: 'Customer',
|
||||
value: 'CUSTOMER',
|
||||
position: 4,
|
||||
},
|
||||
],
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: false,
|
||||
isUnique: false,
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
},
|
||||
'20202020-30a5-4d8e-9b93-12d31ece0aaa': {
|
||||
id: '20202020-30a5-4d8e-9b93-12d31ece0aaa',
|
||||
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
|
||||
type: FieldMetadataType.POSITION,
|
||||
name: 'position',
|
||||
label: 'Position',
|
||||
defaultValue: 0,
|
||||
description: 'Opportunity record position',
|
||||
icon: 'IconHierarchy2',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
isNullable: false,
|
||||
isUnique: false,
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
},
|
||||
'20202020-f95f-424f-ab32-65961e8e9635': {
|
||||
id: '20202020-f95f-424f-ab32-65961e8e9635',
|
||||
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
name: 'createdBy',
|
||||
label: 'Created by',
|
||||
defaultValue: { name: "'System'", source: "'MANUAL'", context: {} },
|
||||
description: 'The creator of the record',
|
||||
icon: 'IconCreativeCommonsSa',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: false,
|
||||
isUnique: false,
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
},
|
||||
'20202020-5e10-4780-babb-38a465ac546c': {
|
||||
id: '20202020-5e10-4780-babb-38a465ac546c',
|
||||
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'searchVector',
|
||||
label: 'Search vector',
|
||||
defaultValue: null,
|
||||
description: 'Field used for full-text search',
|
||||
icon: 'IconUser',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
},
|
||||
'20202020-8f4a-4f8d-822e-90fe72f75b79': {
|
||||
id: '20202020-8f4a-4f8d-822e-90fe72f75b79',
|
||||
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
|
||||
type: FieldMetadataType.UUID,
|
||||
name: 'id',
|
||||
label: 'Id',
|
||||
defaultValue: 'uuid',
|
||||
description: 'Id',
|
||||
icon: 'Icon123',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
isNullable: false,
|
||||
isUnique: false,
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
},
|
||||
'20202020-f120-4b59-b239-f7f1d8eb243e': {
|
||||
id: '20202020-f120-4b59-b239-f7f1d8eb243e',
|
||||
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
name: 'createdAt',
|
||||
label: 'Creation date',
|
||||
defaultValue: 'now',
|
||||
description: 'Creation date',
|
||||
icon: 'IconCalendar',
|
||||
settings: { displayFormat: 'RELATIVE' } as any,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: false,
|
||||
isUnique: false,
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
isLabelSyncedWithName: false,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
},
|
||||
'20202020-dcc8-4318-9756-b87377692561': {
|
||||
id: '20202020-dcc8-4318-9756-b87377692561',
|
||||
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
name: 'updatedAt',
|
||||
label: 'Last update',
|
||||
defaultValue: 'now',
|
||||
description: 'Last time the record was changed',
|
||||
icon: 'IconCalendarClock',
|
||||
settings: { displayFormat: 'RELATIVE' } as any,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: false,
|
||||
isUnique: false,
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
isLabelSyncedWithName: false,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
},
|
||||
'20202020-1694-4f8b-8760-61a5ff330022': {
|
||||
id: '20202020-1694-4f8b-8760-61a5ff330022',
|
||||
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
name: 'deletedAt',
|
||||
label: 'Deleted at',
|
||||
defaultValue: null,
|
||||
description: 'Date when the record was deleted',
|
||||
icon: 'IconCalendarMinus',
|
||||
settings: { displayFormat: 'RELATIVE' } as any,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
},
|
||||
'20202020-4f52-4dea-a116-723f9bf7f082': {
|
||||
id: '20202020-4f52-4dea-a116-723f9bf7f082',
|
||||
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'pointOfContact',
|
||||
label: 'Point of Contact',
|
||||
defaultValue: null,
|
||||
description: 'Opportunity point of contact',
|
||||
icon: 'IconUser',
|
||||
settings: {
|
||||
onDelete: 'SET_NULL',
|
||||
relationType: 'MANY_TO_ONE',
|
||||
joinColumnName: 'pointOfContactId',
|
||||
} as any,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
isLabelSyncedWithName: true,
|
||||
relationTargetFieldMetadataId: '20202020-a36b-4889-97d4-63a578423688',
|
||||
relationTargetObjectMetadataId: '20202020-6799-4a38-92d3-8e844a7ea8ab',
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
},
|
||||
'20202020-fc02-4be2-be1a-e121daf5400d': {
|
||||
id: '20202020-fc02-4be2-be1a-e121daf5400d',
|
||||
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'company',
|
||||
label: 'Company',
|
||||
defaultValue: null,
|
||||
description: 'Opportunity company',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
settings: {
|
||||
onDelete: 'SET_NULL',
|
||||
relationType: 'MANY_TO_ONE',
|
||||
joinColumnName: 'companyId',
|
||||
} as any,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
isLabelSyncedWithName: true,
|
||||
relationTargetFieldMetadataId: '20202020-bd16-4f63-8165-0a7f5d78170d',
|
||||
relationTargetObjectMetadataId: '20202020-0be8-4764-8e0d-7a2e1c66f78c',
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
},
|
||||
'20202020-fd9f-48f0-bd5f-5b0fec6a5de4': {
|
||||
id: '20202020-fd9f-48f0-bd5f-5b0fec6a5de4',
|
||||
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'favorites',
|
||||
label: 'Favorites',
|
||||
defaultValue: null,
|
||||
description: 'Favorites linked to the opportunity',
|
||||
icon: 'IconHeart',
|
||||
settings: { relationType: RelationType.ONE_TO_MANY } as any,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
isLabelSyncedWithName: true,
|
||||
relationTargetFieldMetadataId: '20202020-7c3c-4149-b400-5a958910c7a2',
|
||||
relationTargetObjectMetadataId: '20202020-df10-42dc-baed-ea77db7ad96c',
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
},
|
||||
'20202020-88ab-4138-98ce-80533bb423e3': {
|
||||
id: '20202020-88ab-4138-98ce-80533bb423e3',
|
||||
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'taskTargets',
|
||||
label: 'Tasks',
|
||||
defaultValue: null,
|
||||
description: 'Tasks tied to the opportunity',
|
||||
icon: 'IconCheckbox',
|
||||
settings: { relationType: RelationType.ONE_TO_MANY } as any,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
isLabelSyncedWithName: false,
|
||||
relationTargetFieldMetadataId: '20202020-eb77-4b1c-b6a6-d5dcd13b1634',
|
||||
relationTargetObjectMetadataId: '20202020-3af1-4c4f-90f4-cd43c53f7f41',
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
},
|
||||
'20202020-4258-422b-b35b-db3f090af8da': {
|
||||
id: '20202020-4258-422b-b35b-db3f090af8da',
|
||||
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'noteTargets',
|
||||
label: 'Notes',
|
||||
defaultValue: null,
|
||||
description: 'Notes tied to the opportunity',
|
||||
icon: 'IconNotes',
|
||||
settings: { relationType: RelationType.ONE_TO_MANY } as any,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
isLabelSyncedWithName: false,
|
||||
relationTargetFieldMetadataId: '20202020-d927-4c91-9893-28cea5aff979',
|
||||
relationTargetObjectMetadataId: '20202020-8a5e-4dea-868b-71611e718a73',
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
},
|
||||
'20202020-16ca-40a7-a1ba-712975c916cd': {
|
||||
id: '20202020-16ca-40a7-a1ba-712975c916cd',
|
||||
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'attachments',
|
||||
label: 'Attachments',
|
||||
defaultValue: null,
|
||||
description: 'Attachments linked to the opportunity',
|
||||
icon: 'IconFileImport',
|
||||
settings: { relationType: RelationType.ONE_TO_MANY } as any,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
isLabelSyncedWithName: true,
|
||||
relationTargetFieldMetadataId: '20202020-9236-427a-8a8a-a2296b93b542',
|
||||
relationTargetObjectMetadataId: '20202020-3005-4c93-a04c-2941f7424f54',
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
},
|
||||
'20202020-92a5-47bf-a38d-c1c72b2c3e4d': {
|
||||
id: '20202020-92a5-47bf-a38d-c1c72b2c3e4d',
|
||||
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88',
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'timelineActivities',
|
||||
label: 'Timeline Activities',
|
||||
defaultValue: null,
|
||||
description: 'Timeline Activities linked to the opportunity.',
|
||||
icon: 'IconTimelineEvent',
|
||||
settings: { relationType: RelationType.ONE_TO_MANY } as any,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
isLabelSyncedWithName: true,
|
||||
relationTargetFieldMetadataId: '20202020-2d54-41c7-b886-29deca3c28d5',
|
||||
relationTargetObjectMetadataId: '20202020-a89e-4e4d-b3d9-c3f99e7c7483',
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
},
|
||||
},
|
||||
fieldIdByName: {
|
||||
name: '20202020-c2f1-4435-adca-22931f8b41b6',
|
||||
amount: '20202020-5eef-417a-b517-ebeedaa8e10b',
|
||||
closeDate: '20202020-597c-44d3-98ec-ea71aea5256b',
|
||||
stage: '20202020-9b94-454a-94ca-8afb09c68faf',
|
||||
position: '20202020-30a5-4d8e-9b93-12d31ece0aaa',
|
||||
createdBy: '20202020-f95f-424f-ab32-65961e8e9635',
|
||||
searchVector: '20202020-5e10-4780-babb-38a465ac546c',
|
||||
id: '20202020-8f4a-4f8d-822e-90fe72f75b79',
|
||||
createdAt: '20202020-f120-4b59-b239-f7f1d8eb243e',
|
||||
updatedAt: '20202020-dcc8-4318-9756-b87377692561',
|
||||
deletedAt: '20202020-1694-4f8b-8760-61a5ff330022',
|
||||
pointOfContact: '20202020-4f52-4dea-a116-723f9bf7f082',
|
||||
company: '20202020-fc02-4be2-be1a-e121daf5400d',
|
||||
favorites: '20202020-fd9f-48f0-bd5f-5b0fec6a5de4',
|
||||
taskTargets: '20202020-88ab-4138-98ce-80533bb423e3',
|
||||
noteTargets: '20202020-4258-422b-b35b-db3f090af8da',
|
||||
attachments: '20202020-16ca-40a7-a1ba-712975c916cd',
|
||||
timelineActivities: '20202020-92a5-47bf-a38d-c1c72b2c3e4d',
|
||||
},
|
||||
fieldIdByJoinColumnName: {
|
||||
pointOfContactId: '20202020-4f52-4dea-a116-723f9bf7f082',
|
||||
companyId: '20202020-fc02-4be2-be1a-e121daf5400d',
|
||||
},
|
||||
} as const satisfies ObjectMetadataItemWithFieldMaps;
|
||||
+15
-5
@@ -1,16 +1,22 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
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 { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||
|
||||
export const checkFields = (
|
||||
objectMetadataItem: ObjectMetadataItemWithFieldMaps,
|
||||
objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps,
|
||||
fieldNames: string[],
|
||||
): void => {
|
||||
const fieldMetadataNames = Object.values(objectMetadataItem.fieldsById)
|
||||
.map((field) => {
|
||||
const fieldMetadataNames: string[] = Object.values(
|
||||
objectMetadataItemWithFieldsMaps.fieldsById,
|
||||
)
|
||||
.flatMap((field) => {
|
||||
if (isCompositeFieldMetadataType(field.type)) {
|
||||
const compositeType = compositeTypeDefinitions.get(field.type);
|
||||
|
||||
@@ -29,15 +35,19 @@ export const checkFields = (
|
||||
].flat();
|
||||
}
|
||||
|
||||
if (isFieldMetadataInterfaceOfType(field, FieldMetadataType.RELATION)) {
|
||||
return field.settings?.joinColumnName;
|
||||
}
|
||||
|
||||
return field.name;
|
||||
})
|
||||
.flat();
|
||||
.filter(isDefined);
|
||||
|
||||
for (const fieldName of fieldNames) {
|
||||
if (!fieldMetadataNames.includes(fieldName)) {
|
||||
throw new BadRequestException(
|
||||
`field '${fieldName}' does not exist in '${computeObjectTargetTable(
|
||||
objectMetadataItem,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
)}' object`,
|
||||
);
|
||||
}
|
||||
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
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 { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
|
||||
export const checkArrayFields = (
|
||||
objectMetadataItem: ObjectMetadataItemWithFieldMaps,
|
||||
fields: Array<Partial<ObjectRecord>>,
|
||||
): void => {
|
||||
const fieldMetadataNames = Object.values(objectMetadataItem.fieldsById)
|
||||
.map((field) => {
|
||||
if (isCompositeFieldMetadataType(field.type)) {
|
||||
const compositeType = compositeTypeDefinitions.get(field.type);
|
||||
|
||||
if (!compositeType) {
|
||||
throw new BadRequestException(
|
||||
`Composite type '${field.type}' not found`,
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
field.name,
|
||||
compositeType.properties.map(
|
||||
(compositeProperty) => compositeProperty.name,
|
||||
),
|
||||
].flat();
|
||||
}
|
||||
|
||||
return field.name;
|
||||
})
|
||||
.flat();
|
||||
|
||||
for (const fieldObj of fields) {
|
||||
for (const fieldName in fieldObj) {
|
||||
if (!fieldMetadataNames.includes(fieldName)) {
|
||||
throw new BadRequestException(
|
||||
`field '${fieldName}' does not exist in '${computeObjectTargetTable(
|
||||
objectMetadataItem,
|
||||
)}' object`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
+5
-2
@@ -7,7 +7,7 @@ import {
|
||||
OrderByDirection,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { checkArrayFields } from 'src/engine/api/rest/core/query-builder/utils/check-order-by.utils';
|
||||
import { checkFields } from 'src/engine/api/rest/core/query-builder/utils/check-fields.utils';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
|
||||
@@ -82,7 +82,10 @@ export class OrderByInputFactory {
|
||||
result = [...result, ...resultFields];
|
||||
}
|
||||
|
||||
checkArrayFields(objectMetadata.objectMetadataMapItem, result);
|
||||
checkFields(
|
||||
objectMetadata.objectMetadataMapItem,
|
||||
result.flatMap((fields) => Object.keys(fields)),
|
||||
);
|
||||
|
||||
return this.addDefaultOrderById(result);
|
||||
}
|
||||
|
||||
+10
-26
@@ -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,
|
||||
|
||||
+8
-13
@@ -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,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
+2
@@ -105,4 +105,6 @@ export interface WorkspaceFieldMetadataArgs {
|
||||
* Is active field.
|
||||
*/
|
||||
readonly asExpression?: string;
|
||||
|
||||
readonly isLabelSyncedWithName: boolean;
|
||||
}
|
||||
|
||||
+2
@@ -84,4 +84,6 @@ export interface WorkspaceRelationMetadataArgs {
|
||||
* Is active field.
|
||||
*/
|
||||
readonly isActive?: boolean;
|
||||
|
||||
readonly isLabelSyncedWithName: boolean;
|
||||
}
|
||||
|
||||
+1
-1
@@ -32,6 +32,6 @@ import { WorkspaceFixService } from './services/workspace-fix.service';
|
||||
FieldMetadataHealthService,
|
||||
WorkspaceFixService,
|
||||
],
|
||||
exports: [WorkspaceHealthService],
|
||||
exports: [WorkspaceHealthService, DatabaseStructureService],
|
||||
})
|
||||
export class WorkspaceHealthModule {}
|
||||
|
||||
+95
@@ -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');
|
||||
});
|
||||
});
|
||||
+2
-1
@@ -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`;
|
||||
}
|
||||
|
||||
+4
-3
@@ -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;
|
||||
|
||||
+36
-10
@@ -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: '�\u0002��y�_�\u0013��\x00',
|
||||
expected: '�\u0002��y�_�\u0013��',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
title: 'should sanitize a UCALID with del',
|
||||
context: {
|
||||
input: 'del�\u0002��y�_�\u0013��',
|
||||
expected: 'del�\u0002��y�_�\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);
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -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', '');
|
||||
};
|
||||
|
||||
+28
@@ -197,6 +197,34 @@ describe('Core REST API Find Many endpoint', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should support filtering on a relation field id', async () => {
|
||||
const response = await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
path: `/people?filter=companyId[in]:["${TEST_COMPANY_1_ID}"]`,
|
||||
}).expect(200);
|
||||
|
||||
const filteredPeople = response.body.data.people;
|
||||
|
||||
expect(filteredPeople.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should fail to filter on a relation field name', async () => {
|
||||
const response = await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
path: `/people?filter=company[in]:["${TEST_COMPANY_1_ID}"]`,
|
||||
});
|
||||
|
||||
expect(response.body).toMatchInlineSnapshot(`
|
||||
{
|
||||
"error": "BadRequestException",
|
||||
"messages": [
|
||||
"field 'company' does not exist in 'person' object",
|
||||
],
|
||||
"statusCode": 400,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should support ordering Desc of results', async () => {
|
||||
const descResponse = await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
|
||||
Reference in New Issue
Block a user