Add unique indexes and indexes for composite types (#7162)
Add support for indexes on composite fields and unicity constraint on indexes This pull request includes several changes across multiple files to improve error handling, enforce unique constraints, and update database migrations. The most important changes include updating error messages for snack bars, adding a new command to enforce unique constraints, and updating database migrations to include new fields and constraints. ### Error Handling Improvements: * [`packages/twenty-front/src/modules/error-handler/components/PromiseRejectionEffect.tsx`](diffhunk://#diff-e7dc05ced8e4730430f5c7fcd0c75b3aa723da438c26e0bef8130b614427dd9aL23-R23): Updated error messages in `enqueueSnackBar` to use `error.message` directly. * [`packages/twenty-front/src/modules/object-metadata/hooks/useFindManyObjectMetadataItems.ts`](diffhunk://#diff-74c126d6bc7a5ed6b63be994d298df6669058034bfbc367b11045f9f31a3abe6L44-R46): Simplified error messages in `enqueueSnackBar`. * [`packages/twenty-front/src/modules/object-record/hooks/useFindDuplicateRecords.ts`](diffhunk://#diff-af23a1d99639a66c251f87473e63e2b7bceaa4ee4f70fedfa0fcffe5c7d79181L56-R58): Simplified error messages in `enqueueSnackBar`. * [`packages/twenty-front/src/modules/object-record/hooks/useHandleFindManyRecordsError.ts`](diffhunk://#diff-da04296cbe280202a1eaf6b1244a30490d4f400411bee139651172c59719088eL22-R24): Simplified error messages in `enqueueSnackBar`. ### New Command for Unique Constraints: * [`packages/twenty-server/src/database/commands/upgrade-version/0-31/0-31-enforce-unique-constraints.command.ts`](diffhunk://#diff-8337096c8c80dd2619a5ba691ae5145101f8ae0368a75192a050047e8c6ab7cbR1-R159): Added a new command to enforce unique constraints on company domain names and person emails. * [`packages/twenty-server/src/database/commands/upgrade-version/0-31/0-31-upgrade-version.command.ts`](diffhunk://#diff-20215e9981a53c7566e9cbff96715685125878f5bcb84fe461a7440f2e68f6fcR13-R14): Integrated the new `EnforceUniqueConstraintsCommand` into the upgrade process. [[1]](diffhunk://#diff-20215e9981a53c7566e9cbff96715685125878f5bcb84fe461a7440f2e68f6fcR13-R14) [[2]](diffhunk://#diff-20215e9981a53c7566e9cbff96715685125878f5bcb84fe461a7440f2e68f6fcR31) [[3]](diffhunk://#diff-20215e9981a53c7566e9cbff96715685125878f5bcb84fe461a7440f2e68f6fcR64-R68) * [`packages/twenty-server/src/database/commands/upgrade-version/0-31/0-31-upgrade-version.module.ts`](diffhunk://#diff-da52814efc674c25ed55645f8ee2561013641a407f88423e705dd6c77b405527R7): Registered the new `EnforceUniqueConstraintsCommand` in the module. [[1]](diffhunk://#diff-da52814efc674c25ed55645f8ee2561013641a407f88423e705dd6c77b405527R7) [[2]](diffhunk://#diff-da52814efc674c25ed55645f8ee2561013641a407f88423e705dd6c77b405527R24) ### Database Migrations: * [`packages/twenty-server/src/database/typeorm/metadata/migrations/1726757368824-migrationDebt.ts`](diffhunk://#diff-c450aeae7bc0ef4416a0ade2dc613ca3f688629f35d2a32f90a09c3f494febdcR1-R53): Added a migration to update the `relationMetadata_ondeleteaction_enum` and set default values. * [`packages/twenty-server/src/database/typeorm/metadata/migrations/1726757368825-addIsUniqueToIndexMetadata.ts`](diffhunk://#diff-8f1e14bd7f6835ec2c3bb39bcc51e3c318a3008d576a981e682f4c985e746fbfR1-R19): Added a migration to include the `isUnique` field in `indexMetadata`. * [`packages/twenty-server/src/database/typeorm/metadata/migrations/1726762935841-addCompostiveColumnToIndexFieldMetadata.ts`](diffhunk://#diff-7c96b7276c7722d41ff31de23b2de4d6e09adfdc74815356ba63bc96a2669440R1-R19): Added a migration to include the `compositeColumn` field in `indexFieldMetadata`. * [`packages/twenty-server/src/database/typeorm/metadata/migrations/1726766871572-addWhereToIndexMetadata.ts`](diffhunk://#diff-26651295a975eb50e672dce0e4e274e861f66feb1b68105eee5a04df32796190R1-R14): Added a migration to include the `indexWhereClause` field in `indexMetadata`. ### GraphQL Exception Handling: * [`packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts`](diffhunk://#diff-58445eb362dc89e31107777d39b592d7842d2ab09a223012ccd055da325270a8R1-R4): Enhanced exception handling for `QueryFailedError` to provide more specific error messages for unique constraint violations. [[1]](diffhunk://#diff-58445eb362dc89e31107777d39b592d7842d2ab09a223012ccd055da325270a8R1-R4) [[2]](diffhunk://#diff-58445eb362dc89e31107777d39b592d7842d2ab09a223012ccd055da325270a8R23-R59) * [`packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts`](diffhunk://#diff-233d58ab2333586dd45e46e33d4f07e04a4b8adde4a11a48e25d86985e5a7943L58-R58): Updated the `workspaceQueryRunnerGraphqlApiExceptionHandler` call to include context. * [`packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts`](diffhunk://#diff-68b803f0762c407f5d2d1f5f8d389655a60654a2dd2394a81318655dcd44dc43L58-R58): Updated the `workspaceQueryRunnerGraphqlApiExceptionHandler` call to include context. --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
1
packages/twenty-server/felix
Submodule
1
packages/twenty-server/felix
Submodule
Submodule packages/twenty-server/felix added at a33b017977
@ -7,7 +7,7 @@ import { DataSeedDemoWorkspaceCommand } from 'src/database/commands/data-seed-de
|
||||
import { DataSeedDemoWorkspaceModule } from 'src/database/commands/data-seed-demo-workspace/data-seed-demo-workspace.module';
|
||||
import { DataSeedWorkspaceCommand } from 'src/database/commands/data-seed-dev-workspace.command';
|
||||
import { ConfirmationQuestion } from 'src/database/commands/questions/confirmation.question';
|
||||
import { UpgradeTo0_31CommandModule } from 'src/database/commands/upgrade-version/0-31/0-31-upgrade-version.module';
|
||||
import { UpgradeTo0_32CommandModule } from 'src/database/commands/upgrade-version/0-32/0-32-upgrade-version.module';
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
@ -46,7 +46,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
|
||||
DataSeedDemoWorkspaceModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
WorkspaceMetadataVersionModule,
|
||||
UpgradeTo0_31CommandModule,
|
||||
UpgradeTo0_32CommandModule,
|
||||
FeatureFlagModule,
|
||||
],
|
||||
providers: [
|
||||
|
||||
@ -1,128 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'nest-commander';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveWorkspacesCommandOptions,
|
||||
ActiveWorkspacesCommandRunner,
|
||||
} from 'src/database/commands/active-workspaces.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.31:add-index-key-to-tasks-and-notes-views',
|
||||
description: 'Add index key to tasks and notes views',
|
||||
})
|
||||
export class AddIndexKeyToTasksAndNotesViewsCommand extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
_passedParam: string[],
|
||||
_options: ActiveWorkspacesCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log('Running command to fix migration');
|
||||
|
||||
for (const workspaceId of workspaceIds) {
|
||||
this.logger.log(`Running command for workspace ${workspaceId}`);
|
||||
|
||||
try {
|
||||
this.logger.log(chalk.green(`Cleaning views of ${workspaceId}.`));
|
||||
|
||||
await this.addIndexKeyToTasksAndNotesViews(
|
||||
workspaceId,
|
||||
_options.dryRun ?? false,
|
||||
);
|
||||
|
||||
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
|
||||
workspaceId,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.log(
|
||||
chalk.red(
|
||||
`Running command on workspace ${workspaceId} failed with error: ${error}`,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
} finally {
|
||||
this.logger.log(
|
||||
chalk.green(`Finished running command for workspace ${workspaceId}.`),
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green(`Command completed!`));
|
||||
}
|
||||
}
|
||||
|
||||
private async addIndexKeyToTasksAndNotesViews(
|
||||
workspaceId: string,
|
||||
dryRun: boolean,
|
||||
): Promise<void> {
|
||||
const viewRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'view',
|
||||
false,
|
||||
);
|
||||
|
||||
const allViews = await viewRepository.find();
|
||||
|
||||
const viewObjectMetadataIds = allViews.map((view) => view.objectMetadataId);
|
||||
|
||||
const objectMetadataEntities = await this.objectMetadataRepository.find({
|
||||
where: {
|
||||
id: In(viewObjectMetadataIds),
|
||||
},
|
||||
});
|
||||
|
||||
const tasksAndNotesObjectMetadataIds = objectMetadataEntities.filter(
|
||||
(entity) =>
|
||||
entity.standardId === STANDARD_OBJECT_IDS.task ||
|
||||
entity.standardId === STANDARD_OBJECT_IDS.note,
|
||||
);
|
||||
|
||||
const viewsToUpdate = allViews.filter(
|
||||
(view) =>
|
||||
tasksAndNotesObjectMetadataIds.some(
|
||||
(entity) => entity.id === view.objectMetadataId,
|
||||
) &&
|
||||
['All Tasks', 'All Notes'].includes(view.name) &&
|
||||
view.key === null,
|
||||
);
|
||||
|
||||
if (dryRun) {
|
||||
this.logger.log(
|
||||
chalk.green(
|
||||
`Found ${viewsToUpdate.length} views to update in workspace ${workspaceId}.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (viewsToUpdate.length > 0 && !dryRun) {
|
||||
await viewRepository.update(
|
||||
viewsToUpdate.map((view) => view.id),
|
||||
{
|
||||
key: 'INDEX',
|
||||
},
|
||||
);
|
||||
this.logger.log(chalk.green(`Updating ${viewsToUpdate.length} views.`));
|
||||
}
|
||||
|
||||
if (viewsToUpdate.length === 0 && !dryRun) {
|
||||
this.logger.log(chalk.green(`No views to update.`));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,162 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'nest-commander';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveWorkspacesCommandOptions,
|
||||
ActiveWorkspacesCommandRunner,
|
||||
} from 'src/database/commands/active-workspaces.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
|
||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.31:backfill-workspace-favorites-migration',
|
||||
description: 'Create a workspace favorite for all workspace views',
|
||||
})
|
||||
export class BackfillWorkspaceFavoritesCommand extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
_passedParam: string[],
|
||||
_options: ActiveWorkspacesCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log('Running command to fix migration');
|
||||
|
||||
for (const workspaceId of workspaceIds) {
|
||||
this.logger.log(`Running command for workspace ${workspaceId}`);
|
||||
|
||||
try {
|
||||
const allWorkspaceIndexViews = await this.getIndexViews(workspaceId);
|
||||
|
||||
const activeWorkspaceIndexViews =
|
||||
await this.filterViewsWithoutObjectMetadata(
|
||||
workspaceId,
|
||||
allWorkspaceIndexViews,
|
||||
);
|
||||
|
||||
await this.createViewWorkspaceFavorites(
|
||||
workspaceId,
|
||||
activeWorkspaceIndexViews.map((view) => view.id),
|
||||
_options.dryRun ?? false,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
chalk.green(`Backfilled workspace favorites to ${workspaceId}.`),
|
||||
);
|
||||
|
||||
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
|
||||
workspaceId,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.log(
|
||||
chalk.red(
|
||||
`Running command on workspace ${workspaceId} failed with error: ${error}`,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
} finally {
|
||||
this.logger.log(
|
||||
chalk.green(`Finished running command for workspace ${workspaceId}.`),
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green(`Command completed!`));
|
||||
}
|
||||
}
|
||||
|
||||
private async getIndexViews(
|
||||
workspaceId: string,
|
||||
): Promise<ViewWorkspaceEntity[]> {
|
||||
const viewRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'view',
|
||||
false,
|
||||
);
|
||||
|
||||
return viewRepository.find({
|
||||
where: {
|
||||
key: 'INDEX',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async filterViewsWithoutObjectMetadata(
|
||||
workspaceId: string,
|
||||
views: ViewWorkspaceEntity[],
|
||||
): Promise<ViewWorkspaceEntity[]> {
|
||||
const viewObjectMetadataIds = views.map((view) => view.objectMetadataId);
|
||||
|
||||
const objectMetadataEntities = await this.objectMetadataRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
id: In(viewObjectMetadataIds),
|
||||
},
|
||||
});
|
||||
|
||||
const objectMetadataIds = new Set(
|
||||
objectMetadataEntities.map((entity) => entity.id),
|
||||
);
|
||||
|
||||
return views.filter((view) => objectMetadataIds.has(view.objectMetadataId));
|
||||
}
|
||||
|
||||
private async createViewWorkspaceFavorites(
|
||||
workspaceId: string,
|
||||
viewIds: string[],
|
||||
dryRun: boolean,
|
||||
) {
|
||||
const favoriteRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<FavoriteWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'favorite',
|
||||
);
|
||||
|
||||
let nextFavoritePosition = await favoriteRepository.count();
|
||||
let createdFavorites = 0;
|
||||
|
||||
for (const viewId of viewIds) {
|
||||
const existingFavorites = await favoriteRepository.find({
|
||||
where: {
|
||||
viewId,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingFavorites.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
await favoriteRepository.insert(
|
||||
favoriteRepository.create({
|
||||
viewId,
|
||||
position: nextFavoritePosition,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
createdFavorites++;
|
||||
nextFavoritePosition++;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
chalk.green(
|
||||
`Found ${createdFavorites} favorites to backfill in workspace ${workspaceId}.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,119 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'nest-commander';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveWorkspacesCommandOptions,
|
||||
ActiveWorkspacesCommandRunner,
|
||||
} from 'src/database/commands/active-workspaces.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.31:clean-views-associated-with-outdated-objects',
|
||||
description:
|
||||
'Clean views associated with deleted object metadata or activities',
|
||||
})
|
||||
export class CleanViewsAssociatedWithOutdatedObjectsCommand extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
_passedParam: string[],
|
||||
_options: ActiveWorkspacesCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log('Running command to fix migration');
|
||||
|
||||
for (const workspaceId of workspaceIds) {
|
||||
this.logger.log(`Running command for workspace ${workspaceId}`);
|
||||
|
||||
try {
|
||||
this.logger.log(chalk.green(`Cleaning views of ${workspaceId}.`));
|
||||
|
||||
await this.cleanViewsWithDeletedObjectMetadata(
|
||||
workspaceId,
|
||||
_options.dryRun ?? false,
|
||||
);
|
||||
|
||||
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
|
||||
workspaceId,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.log(
|
||||
chalk.red(
|
||||
`Running command on workspace ${workspaceId} failed with error: ${error}`,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
} finally {
|
||||
this.logger.log(
|
||||
chalk.green(`Finished running command for workspace ${workspaceId}.`),
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green(`Command completed!`));
|
||||
}
|
||||
}
|
||||
|
||||
private async cleanViewsWithDeletedObjectMetadata(
|
||||
workspaceId: string,
|
||||
dryRun: boolean,
|
||||
): Promise<void> {
|
||||
const viewRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'view',
|
||||
false,
|
||||
);
|
||||
|
||||
const allViews = await viewRepository.find();
|
||||
|
||||
const viewObjectMetadataIds = allViews.map((view) => view.objectMetadataId);
|
||||
|
||||
const objectMetadataEntities = await this.objectMetadataRepository.find({
|
||||
where: {
|
||||
id: In(viewObjectMetadataIds),
|
||||
},
|
||||
});
|
||||
|
||||
const validObjectMetadataIds = new Set(
|
||||
objectMetadataEntities
|
||||
.filter((entity) => entity.standardId !== STANDARD_OBJECT_IDS.activity)
|
||||
.map((entity) => entity.id),
|
||||
);
|
||||
|
||||
const viewIdsToDelete = allViews
|
||||
.filter((view) => !validObjectMetadataIds.has(view.objectMetadataId))
|
||||
.map((view) => view.id);
|
||||
|
||||
if (dryRun) {
|
||||
this.logger.log(
|
||||
chalk.green(
|
||||
`Found ${viewIdsToDelete.length} views to clean in workspace ${workspaceId}.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (viewIdsToDelete.length > 0 && !dryRun) {
|
||||
await viewRepository.delete(viewIdsToDelete);
|
||||
this.logger.log(chalk.green(`Cleaning ${viewIdsToDelete.length} views.`));
|
||||
}
|
||||
|
||||
if (viewIdsToDelete.length === 0) {
|
||||
this.logger.log(chalk.green(`No views to clean.`));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,121 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'nest-commander';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveWorkspacesCommandOptions,
|
||||
ActiveWorkspacesCommandRunner,
|
||||
} from 'src/database/commands/active-workspaces.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.31:delete-name-column-standard-object-tables',
|
||||
description: 'Delete name column from standard object tables',
|
||||
})
|
||||
export class DeleteNameColumnStandardObjectTablesCommand extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
_passedParam: string[],
|
||||
options: ActiveWorkspacesCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log('Running command to fix migration');
|
||||
|
||||
for (const workspaceId of workspaceIds) {
|
||||
this.logger.log(`Running command for workspace ${workspaceId}`);
|
||||
|
||||
try {
|
||||
this.logger.log(
|
||||
chalk.green(`Deleting name columns from workspace ${workspaceId}.`),
|
||||
);
|
||||
|
||||
const standardObjects = await this.objectMetadataRepository.find({
|
||||
where: {
|
||||
isCustom: false,
|
||||
workspaceId,
|
||||
},
|
||||
relations: ['fields'],
|
||||
});
|
||||
|
||||
const dataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
dataSource.transaction(async (entityManager) => {
|
||||
const queryRunner = entityManager.queryRunner;
|
||||
|
||||
for (const standardObject of standardObjects) {
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
chalk.yellow(
|
||||
`Dry run mode enabled. Skipping deletion of name column for workspace ${workspaceId} and table ${standardObject.nameSingular}.`,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const nameColumnExists = await queryRunner?.hasColumn(
|
||||
standardObject.nameSingular,
|
||||
'name',
|
||||
);
|
||||
|
||||
const nameFieldMetadataExists = standardObject.fields.some(
|
||||
(field) =>
|
||||
field.name === 'name' && field.type === FieldMetadataType.TEXT,
|
||||
);
|
||||
|
||||
if (nameFieldMetadataExists) {
|
||||
this.logger.log(
|
||||
chalk.yellow(
|
||||
`Name field exists for workspace ${workspaceId} and table ${standardObject.nameSingular}. Skipping deletion.`,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!nameColumnExists) {
|
||||
this.logger.log(
|
||||
chalk.yellow(
|
||||
`Name column does not exist for workspace ${workspaceId} and table ${standardObject.nameSingular}. Skipping deletion.`,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
await queryRunner?.dropColumn(standardObject.nameSingular, 'name');
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.log(
|
||||
chalk.red(
|
||||
`Running command on workspace ${workspaceId} failed with error: ${error}`,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
} finally {
|
||||
this.logger.log(
|
||||
chalk.green(`Finished running command for workspace ${workspaceId}.`),
|
||||
);
|
||||
|
||||
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,69 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
|
||||
import { AddIndexKeyToTasksAndNotesViewsCommand } from 'src/database/commands/upgrade-version/0-31/0-31-add-index-key-to-tasks-and-notes-views.command';
|
||||
import { BackfillWorkspaceFavoritesCommand } from 'src/database/commands/upgrade-version/0-31/0-31-backfill-workspace-favorites.command';
|
||||
import { CleanViewsAssociatedWithOutdatedObjectsCommand } from 'src/database/commands/upgrade-version/0-31/0-31-clean-views-associated-with-outdated-objects.command';
|
||||
import { DeleteNameColumnStandardObjectTablesCommand } from 'src/database/commands/upgrade-version/0-31/0-31-delete-name-column-standard-object-tables.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
|
||||
|
||||
interface UpdateTo0_31CommandOptions {
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.31',
|
||||
description: 'Upgrade to 0.31',
|
||||
})
|
||||
export class UpgradeTo0_31Command extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
|
||||
private readonly backfillWorkspaceFavoritesCommand: BackfillWorkspaceFavoritesCommand,
|
||||
private readonly cleanViewsAssociatedWithOutdatedObjectsCommand: CleanViewsAssociatedWithOutdatedObjectsCommand,
|
||||
private readonly addIndexKeyToTasksAndNotesViewsCommand: AddIndexKeyToTasksAndNotesViewsCommand,
|
||||
private readonly deleteNameColumnStandardObjectTablesCommand: DeleteNameColumnStandardObjectTablesCommand,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
passedParam: string[],
|
||||
options: UpdateTo0_31CommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
await this.syncWorkspaceMetadataCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
{
|
||||
...options,
|
||||
force: true,
|
||||
},
|
||||
workspaceIds,
|
||||
);
|
||||
await this.cleanViewsAssociatedWithOutdatedObjectsCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
await this.addIndexKeyToTasksAndNotesViewsCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
await this.backfillWorkspaceFavoritesCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
await this.deleteNameColumnStandardObjectTablesCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AddIndexKeyToTasksAndNotesViewsCommand } from 'src/database/commands/upgrade-version/0-31/0-31-add-index-key-to-tasks-and-notes-views.command';
|
||||
import { BackfillWorkspaceFavoritesCommand } from 'src/database/commands/upgrade-version/0-31/0-31-backfill-workspace-favorites.command';
|
||||
import { CleanViewsAssociatedWithOutdatedObjectsCommand } from 'src/database/commands/upgrade-version/0-31/0-31-clean-views-associated-with-outdated-objects.command';
|
||||
import { DeleteNameColumnStandardObjectTablesCommand } from 'src/database/commands/upgrade-version/0-31/0-31-delete-name-column-standard-object-tables.command';
|
||||
import { UpgradeTo0_31Command } from 'src/database/commands/upgrade-version/0-31/0-31-upgrade-version.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Workspace], 'core'),
|
||||
TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
|
||||
WorkspaceSyncMetadataCommandsModule,
|
||||
],
|
||||
providers: [
|
||||
UpgradeTo0_31Command,
|
||||
BackfillWorkspaceFavoritesCommand,
|
||||
CleanViewsAssociatedWithOutdatedObjectsCommand,
|
||||
AddIndexKeyToTasksAndNotesViewsCommand,
|
||||
DeleteNameColumnStandardObjectTablesCommand,
|
||||
],
|
||||
})
|
||||
export class UpgradeTo0_31CommandModule {}
|
||||
@ -0,0 +1,303 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command, Option } from 'nest-commander';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveWorkspacesCommandOptions,
|
||||
ActiveWorkspacesCommandRunner,
|
||||
} from 'src/database/commands/active-workspaces.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
interface EnforceUniqueConstraintsCommandOptions
|
||||
extends ActiveWorkspacesCommandOptions {
|
||||
person?: boolean;
|
||||
company?: boolean;
|
||||
viewField?: boolean;
|
||||
viewSort?: boolean;
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.32:enforce-unique-constraints',
|
||||
description:
|
||||
'Enforce unique constraints on company domainName, person emailsPrimaryEmail, ViewField, and ViewSort',
|
||||
})
|
||||
export class EnforceUniqueConstraintsCommand extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '--person',
|
||||
description: 'Enforce unique constraints on person emailsPrimaryEmail',
|
||||
})
|
||||
parsePerson() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '--company',
|
||||
description: 'Enforce unique constraints on company domainName',
|
||||
})
|
||||
parseCompany() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '--view-field',
|
||||
description: 'Enforce unique constraints on ViewField',
|
||||
})
|
||||
parseViewField() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '--view-sort',
|
||||
description: 'Enforce unique constraints on ViewSort',
|
||||
})
|
||||
parseViewSort() {
|
||||
return true;
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
_passedParam: string[],
|
||||
options: EnforceUniqueConstraintsCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log('Running command to enforce unique constraints');
|
||||
|
||||
for (const workspaceId of workspaceIds) {
|
||||
this.logger.log(`Running command for workspace ${workspaceId}`);
|
||||
|
||||
try {
|
||||
await this.enforceUniqueConstraintsForWorkspace(
|
||||
workspaceId,
|
||||
options.dryRun ?? false,
|
||||
options,
|
||||
);
|
||||
|
||||
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
|
||||
workspaceId,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.log(
|
||||
chalk.red(
|
||||
`Running command on workspace ${workspaceId} failed with error: ${error}, ${error.stack}`,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
} finally {
|
||||
this.logger.log(
|
||||
chalk.green(`Finished running command for workspace ${workspaceId}.`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green(`Command completed!`));
|
||||
}
|
||||
|
||||
private async enforceUniqueConstraintsForWorkspace(
|
||||
workspaceId: string,
|
||||
dryRun: boolean,
|
||||
options: EnforceUniqueConstraintsCommandOptions,
|
||||
): Promise<void> {
|
||||
if (options.company) {
|
||||
await this.enforceUniqueCompanyDomainName(workspaceId, dryRun);
|
||||
}
|
||||
if (options.person) {
|
||||
await this.enforceUniquePersonEmail(workspaceId, dryRun);
|
||||
}
|
||||
if (options.viewField) {
|
||||
await this.enforceUniqueViewField(workspaceId, dryRun);
|
||||
}
|
||||
if (options.viewSort) {
|
||||
await this.enforceUniqueViewSort(workspaceId, dryRun);
|
||||
}
|
||||
}
|
||||
|
||||
private async enforceUniqueCompanyDomainName(
|
||||
workspaceId: string,
|
||||
dryRun: boolean,
|
||||
): Promise<void> {
|
||||
const companyRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'company',
|
||||
);
|
||||
|
||||
const duplicates = await companyRepository
|
||||
.createQueryBuilder('company')
|
||||
.select('company.domainNamePrimaryLinkUrl')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('company.deletedAt IS NULL')
|
||||
.where('company.domainNamePrimaryLinkUrl IS NOT NULL')
|
||||
.where("company.domainNamePrimaryLinkUrl != ''")
|
||||
.groupBy('company.domainNamePrimaryLinkUrl')
|
||||
.having('COUNT(*) > 1')
|
||||
.getRawMany();
|
||||
|
||||
for (const duplicate of duplicates) {
|
||||
const { company_domainNamePrimaryLinkUrl } = duplicate;
|
||||
const companies = await companyRepository.find({
|
||||
where: {
|
||||
domainName: {
|
||||
primaryLinkUrl: company_domainNamePrimaryLinkUrl,
|
||||
},
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
for (let i = 1; i < companies.length; i++) {
|
||||
const newdomainNamePrimaryLinkUrl = `${company_domainNamePrimaryLinkUrl}${i}`;
|
||||
|
||||
if (!dryRun) {
|
||||
await companyRepository.update(companies[i].id, {
|
||||
domainNamePrimaryLinkUrl: newdomainNamePrimaryLinkUrl,
|
||||
});
|
||||
}
|
||||
this.logger.log(
|
||||
chalk.yellow(
|
||||
`Updated company ${companies[i].id} domainName from ${company_domainNamePrimaryLinkUrl} to ${newdomainNamePrimaryLinkUrl}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async enforceUniquePersonEmail(
|
||||
workspaceId: string,
|
||||
dryRun: boolean,
|
||||
): Promise<void> {
|
||||
const personRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'person',
|
||||
);
|
||||
|
||||
const duplicates = await personRepository
|
||||
.createQueryBuilder('person')
|
||||
.select('person.emailsPrimaryEmail')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('person.deletedAt IS NULL')
|
||||
.where('person.emailsPrimaryEmail IS NOT NULL')
|
||||
.where("person.emailsPrimaryEmail != ''")
|
||||
.groupBy('person.emailsPrimaryEmail')
|
||||
.having('COUNT(*) > 1')
|
||||
.getRawMany();
|
||||
|
||||
for (const duplicate of duplicates) {
|
||||
const { person_emailsPrimaryEmail } = duplicate;
|
||||
const persons = await personRepository.find({
|
||||
where: {
|
||||
emails: {
|
||||
primaryEmail: person_emailsPrimaryEmail,
|
||||
},
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
for (let i = 1; i < persons.length; i++) {
|
||||
const newEmail = person_emailsPrimaryEmail?.includes('@')
|
||||
? `${person_emailsPrimaryEmail.split('@')[0]}+${i}@${person_emailsPrimaryEmail.split('@')[1]}`
|
||||
: `${person_emailsPrimaryEmail}+${i}`;
|
||||
|
||||
if (!dryRun) {
|
||||
await personRepository.update(persons[i].id, {
|
||||
emailsPrimaryEmail: newEmail,
|
||||
});
|
||||
}
|
||||
this.logger.log(
|
||||
chalk.yellow(
|
||||
`Updated person ${persons[i].id} emailsPrimaryEmail from ${person_emailsPrimaryEmail} to ${newEmail}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async enforceUniqueViewField(
|
||||
workspaceId: string,
|
||||
dryRun: boolean,
|
||||
): Promise<void> {
|
||||
const viewFieldRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'viewField',
|
||||
);
|
||||
|
||||
const duplicates = await viewFieldRepository
|
||||
.createQueryBuilder('viewField')
|
||||
.select(['viewField.fieldMetadataId', 'viewField.viewId'])
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('viewField.deletedAt IS NULL')
|
||||
.groupBy('viewField.fieldMetadataId, viewField.viewId')
|
||||
.having('COUNT(*) > 1')
|
||||
.getRawMany();
|
||||
|
||||
for (const duplicate of duplicates) {
|
||||
const { fieldMetadataId, viewId } = duplicate;
|
||||
const viewFields = await viewFieldRepository.find({
|
||||
where: { fieldMetadataId, viewId, deletedAt: IsNull() },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
for (let i = 1; i < viewFields.length; i++) {
|
||||
if (!dryRun) {
|
||||
await viewFieldRepository.softDelete(viewFields[i].id);
|
||||
}
|
||||
this.logger.log(
|
||||
chalk.yellow(
|
||||
`Soft deleted duplicate ViewField ${viewFields[i].id} for fieldMetadataId ${fieldMetadataId} and viewId ${viewId}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async enforceUniqueViewSort(
|
||||
workspaceId: string,
|
||||
dryRun: boolean,
|
||||
): Promise<void> {
|
||||
const viewSortRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'viewSort',
|
||||
);
|
||||
|
||||
const duplicates = await viewSortRepository
|
||||
.createQueryBuilder('viewSort')
|
||||
.select(['viewSort.fieldMetadataId', 'viewSort.viewId'])
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('viewSort.deletedAt IS NULL')
|
||||
.groupBy('viewSort.fieldMetadataId, viewSort.viewId')
|
||||
.having('COUNT(*) > 1')
|
||||
.getRawMany();
|
||||
|
||||
for (const duplicate of duplicates) {
|
||||
const { fieldMetadataId, viewId } = duplicate;
|
||||
const viewSorts = await viewSortRepository.find({
|
||||
where: { fieldMetadataId, viewId, deletedAt: IsNull() },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
for (let i = 1; i < viewSorts.length; i++) {
|
||||
if (!dryRun) {
|
||||
await viewSortRepository.softDelete(viewSorts[i].id);
|
||||
}
|
||||
this.logger.log(
|
||||
chalk.yellow(
|
||||
`Soft deleted duplicate ViewSort ${viewSorts[i].id} for fieldMetadataId ${fieldMetadataId} and viewId ${viewId}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
|
||||
|
||||
import { EnforceUniqueConstraintsCommand } from './0-32-enforce-unique-constraints.command';
|
||||
|
||||
interface UpdateTo0_32CommandOptions {
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.32',
|
||||
description: 'Upgrade to 0.32',
|
||||
})
|
||||
export class UpgradeTo0_32Command extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
|
||||
private readonly enforceUniqueConstraintsCommand: EnforceUniqueConstraintsCommand,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
passedParam: string[],
|
||||
options: UpdateTo0_32CommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
await this.syncWorkspaceMetadataCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
{
|
||||
...options,
|
||||
force: true,
|
||||
},
|
||||
workspaceIds,
|
||||
);
|
||||
|
||||
await this.enforceUniqueConstraintsCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { EnforceUniqueConstraintsCommand } from 'src/database/commands/upgrade-version/0-32/0-32-enforce-unique-constraints.command';
|
||||
import { UpgradeTo0_32Command } from 'src/database/commands/upgrade-version/0-32/0-32-upgrade-version.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Workspace], 'core'),
|
||||
TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
|
||||
WorkspaceSyncMetadataCommandsModule,
|
||||
],
|
||||
providers: [UpgradeTo0_32Command, EnforceUniqueConstraintsCommand],
|
||||
})
|
||||
export class UpgradeTo0_32CommandModule {}
|
||||
@ -43,7 +43,7 @@ export const seedFeatureFlags = async (
|
||||
{
|
||||
key: FeatureFlagKey.IsWorkflowEnabled,
|
||||
workspaceId: workspaceId,
|
||||
value: false,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IsMessageThreadSubscriberEnabled,
|
||||
@ -53,7 +53,7 @@ export const seedFeatureFlags = async (
|
||||
{
|
||||
key: FeatureFlagKey.IsWorkspaceFavoriteEnabled,
|
||||
workspaceId: workspaceId,
|
||||
value: false,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IsSearchEnabled,
|
||||
@ -75,6 +75,11 @@ export const seedFeatureFlags = async (
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IsUniqueIndexesEnabled,
|
||||
workspaceId: workspaceId,
|
||||
value: false,
|
||||
},
|
||||
])
|
||||
.execute();
|
||||
};
|
||||
|
||||
@ -15,6 +15,7 @@ export const getDevSeedCompanyCustomFields = (
|
||||
icon: 'IconAdCircle',
|
||||
isActive: true,
|
||||
isNullable: false,
|
||||
isUnique: false,
|
||||
defaultValue: "''",
|
||||
objectMetadataId,
|
||||
},
|
||||
@ -27,6 +28,7 @@ export const getDevSeedCompanyCustomFields = (
|
||||
icon: 'IconVideo',
|
||||
isActive: true,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
objectMetadataId,
|
||||
},
|
||||
{
|
||||
@ -38,6 +40,7 @@ export const getDevSeedCompanyCustomFields = (
|
||||
icon: 'IconHome',
|
||||
isActive: true,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
objectMetadataId,
|
||||
options: [
|
||||
{
|
||||
@ -69,6 +72,7 @@ export const getDevSeedCompanyCustomFields = (
|
||||
icon: 'IconBrandVisa',
|
||||
isActive: true,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
objectMetadataId,
|
||||
defaultValue: false,
|
||||
},
|
||||
@ -89,6 +93,7 @@ export const getDevSeedPeopleCustomFields = (
|
||||
icon: 'IconNote',
|
||||
isActive: true,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
objectMetadataId,
|
||||
},
|
||||
{
|
||||
@ -100,6 +105,7 @@ export const getDevSeedPeopleCustomFields = (
|
||||
icon: 'IconBrandWhatsapp',
|
||||
isActive: true,
|
||||
isNullable: false,
|
||||
isUnique: false,
|
||||
defaultValue: [
|
||||
{
|
||||
primaryPhoneNumber: '',
|
||||
@ -118,6 +124,7 @@ export const getDevSeedPeopleCustomFields = (
|
||||
icon: 'IconHome',
|
||||
isActive: true,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
objectMetadataId,
|
||||
options: [
|
||||
{
|
||||
@ -149,6 +156,7 @@ export const getDevSeedPeopleCustomFields = (
|
||||
icon: 'IconStars',
|
||||
isActive: true,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
objectMetadataId,
|
||||
options: [
|
||||
{
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class MigrationDebt1726757368824 implements MigrationInterface {
|
||||
name = 'MigrationDebt1726757368824';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TYPE "metadata"."relationMetadata_ondeleteaction_enum" RENAME TO "relationMetadata_ondeleteaction_enum_old"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "metadata"."relationMetadata_ondeleteaction_enum" AS ENUM('CASCADE', 'RESTRICT', 'SET_NULL', 'NO_ACTION')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."relationMetadata" ALTER COLUMN "onDeleteAction" DROP DEFAULT`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."relationMetadata" ALTER COLUMN "onDeleteAction" TYPE "metadata"."relationMetadata_ondeleteaction_enum" USING "onDeleteAction"::"text"::"metadata"."relationMetadata_ondeleteaction_enum"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."relationMetadata" ALTER COLUMN "onDeleteAction" SET DEFAULT 'SET_NULL'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "metadata"."relationMetadata_ondeleteaction_enum_old"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."workspaceMigration" ALTER COLUMN "name" SET NOT NULL`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."workspaceMigration" ALTER COLUMN "name" DROP NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "metadata"."relationMetadata_ondeleteaction_enum_old" AS ENUM('CASCADE', 'RESTRICT', 'SET_NULL')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."relationMetadata" ALTER COLUMN "onDeleteAction" DROP DEFAULT`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."relationMetadata" ALTER COLUMN "onDeleteAction" TYPE "metadata"."relationMetadata_ondeleteaction_enum_old" USING "onDeleteAction"::"text"::"metadata"."relationMetadata_ondeleteaction_enum_old"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."relationMetadata" ALTER COLUMN "onDeleteAction" SET DEFAULT 'SET_NULL'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "metadata"."relationMetadata_ondeleteaction_enum"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TYPE "metadata"."relationMetadata_ondeleteaction_enum_old" RENAME TO "relationMetadata_ondeleteaction_enum"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddIsUniqueToIndexMetadata1726757368825
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddIsUniqueToIndexMetadata1726757368825';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."indexMetadata" ADD "isUnique" boolean NOT NULL DEFAULT false`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."indexMetadata" DROP COLUMN "isUnique"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddWhereToIndexMetadata1726766871572
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddWhereToIndexMetadata1726766871572';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."indexMetadata" ADD "indexWhereClause" text`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."indexMetadata" DROP COLUMN "indexWhereClause"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddIsUniqueToFields1728563893694 implements MigrationInterface {
|
||||
name = 'AddIsUniqueToFields1728563893694';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."fieldMetadata" ADD "isUnique" boolean DEFAULT false`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."indexMetadata" DROP COLUMN "indexWhereClause"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { ObjectLiteral, WhereExpressionBuilder } from 'typeorm';
|
||||
import { WhereExpressionBuilder } from 'typeorm';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
@ -6,17 +6,13 @@ import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { computeWhereConditionParts } from 'src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts';
|
||||
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 { FieldMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
|
||||
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
|
||||
import { capitalize } from 'src/utils/capitalize';
|
||||
|
||||
type WhereConditionParts = {
|
||||
sql: string;
|
||||
params: ObjectLiteral;
|
||||
};
|
||||
|
||||
export class GraphqlQueryFilterFieldParser {
|
||||
private fieldMetadataMap: FieldMetadataMap;
|
||||
|
||||
@ -57,7 +53,7 @@ export class GraphqlQueryFilterFieldParser {
|
||||
}
|
||||
}
|
||||
|
||||
const { sql, params } = this.computeWhereConditionParts(
|
||||
const { sql, params } = computeWhereConditionParts(
|
||||
operator,
|
||||
objectNameSingular,
|
||||
key,
|
||||
@ -71,83 +67,6 @@ export class GraphqlQueryFilterFieldParser {
|
||||
}
|
||||
}
|
||||
|
||||
private computeWhereConditionParts(
|
||||
operator: string,
|
||||
objectNameSingular: string,
|
||||
key: string,
|
||||
value: any,
|
||||
): WhereConditionParts {
|
||||
const uuid = Math.random().toString(36).slice(2, 7);
|
||||
|
||||
switch (operator) {
|
||||
case 'eq':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" = :${key}${uuid}`,
|
||||
params: { [`${key}${uuid}`]: value },
|
||||
};
|
||||
case 'neq':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" != :${key}${uuid}`,
|
||||
params: { [`${key}${uuid}`]: value },
|
||||
};
|
||||
case 'gt':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" > :${key}${uuid}`,
|
||||
params: { [`${key}${uuid}`]: value },
|
||||
};
|
||||
case 'gte':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" >= :${key}${uuid}`,
|
||||
params: { [`${key}${uuid}`]: value },
|
||||
};
|
||||
case 'lt':
|
||||
return {
|
||||
sql: `"${objectNameSingular}".${key} < :${key}${uuid}`,
|
||||
params: { [`${key}${uuid}`]: value },
|
||||
};
|
||||
case 'lte':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" <= :${key}${uuid}`,
|
||||
params: { [`${key}${uuid}`]: value },
|
||||
};
|
||||
case 'in':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" IN (:...${key}${uuid})`,
|
||||
params: { [`${key}${uuid}`]: value },
|
||||
};
|
||||
case 'is':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" IS ${value === 'NULL' ? 'NULL' : 'NOT NULL'}`,
|
||||
params: {},
|
||||
};
|
||||
case 'like':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" LIKE :${key}${uuid}`,
|
||||
params: { [`${key}${uuid}`]: `${value}` },
|
||||
};
|
||||
case 'ilike':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" ILIKE :${key}${uuid}`,
|
||||
params: { [`${key}${uuid}`]: `${value}` },
|
||||
};
|
||||
case 'startsWith':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" LIKE :${key}${uuid}`,
|
||||
params: { [`${key}${uuid}`]: `${value}` },
|
||||
};
|
||||
case 'endsWith':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" LIKE :${key}${uuid}`,
|
||||
params: { [`${key}${uuid}`]: `${value}` },
|
||||
};
|
||||
default:
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Operator "${operator}" is not supported`,
|
||||
GraphqlQueryRunnerExceptionCode.UNSUPPORTED_OPERATOR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private parseCompositeFieldForFilter(
|
||||
queryBuilder: WhereExpressionBuilder,
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
@ -182,7 +101,7 @@ export class GraphqlQueryFilterFieldParser {
|
||||
subFieldFilter as Record<string, any>,
|
||||
);
|
||||
|
||||
const { sql, params } = this.computeWhereConditionParts(
|
||||
const { sql, params } = computeWhereConditionParts(
|
||||
operator,
|
||||
objectNameSingular,
|
||||
fullFieldName,
|
||||
|
||||
@ -107,6 +107,7 @@ export class GraphqlQueryCreateManyResolverService
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<void> {
|
||||
assertMutationNotOnRemoteObject(options.objectMetadataItem);
|
||||
|
||||
args.data.forEach((record) => {
|
||||
if (record?.id) {
|
||||
assertIsValidUuid(record.id);
|
||||
|
||||
@ -0,0 +1,88 @@
|
||||
import { ObjectLiteral } from 'typeorm';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
|
||||
type WhereConditionParts = {
|
||||
sql: string;
|
||||
params: ObjectLiteral;
|
||||
};
|
||||
|
||||
export const computeWhereConditionParts = (
|
||||
operator: string,
|
||||
objectNameSingular: string,
|
||||
key: string,
|
||||
value: any,
|
||||
): WhereConditionParts => {
|
||||
const uuid = Math.random().toString(36).slice(2, 7);
|
||||
|
||||
switch (operator) {
|
||||
case 'eq':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" = :${key}${uuid}`,
|
||||
params: { [`${key}${uuid}`]: value },
|
||||
};
|
||||
case 'neq':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" != :${key}${uuid}`,
|
||||
params: { [`${key}${uuid}`]: value },
|
||||
};
|
||||
case 'gt':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" > :${key}${uuid}`,
|
||||
params: { [`${key}${uuid}`]: value },
|
||||
};
|
||||
case 'gte':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" >= :${key}${uuid}`,
|
||||
params: { [`${key}${uuid}`]: value },
|
||||
};
|
||||
case 'lt':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" < :${key}${uuid}`,
|
||||
params: { [`${key}${uuid}`]: value },
|
||||
};
|
||||
case 'lte':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" <= :${key}${uuid}`,
|
||||
params: { [`${key}${uuid}`]: value },
|
||||
};
|
||||
case 'in':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" IN (:...${key}${uuid})`,
|
||||
params: { [`${key}${uuid}`]: value },
|
||||
};
|
||||
case 'is':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" IS ${value === 'NULL' ? 'NULL' : 'NOT NULL'}`,
|
||||
params: {},
|
||||
};
|
||||
case 'like':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" LIKE :${key}${uuid}`,
|
||||
params: { [`${key}${uuid}`]: `${value}` },
|
||||
};
|
||||
case 'ilike':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" ILIKE :${key}${uuid}`,
|
||||
params: { [`${key}${uuid}`]: `${value}` },
|
||||
};
|
||||
case 'startsWith':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" LIKE :${key}${uuid}`,
|
||||
params: { [`${key}${uuid}`]: `${value}` },
|
||||
};
|
||||
case 'endsWith':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" LIKE :${key}${uuid}`,
|
||||
params: { [`${key}${uuid}`]: `${value}` },
|
||||
};
|
||||
default:
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Operator "${operator}" is not supported`,
|
||||
GraphqlQueryRunnerExceptionCode.UNSUPPORTED_OPERATOR,
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -1,3 +1,7 @@
|
||||
import { QueryFailedError } from 'typeorm';
|
||||
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
@ -16,7 +20,51 @@ import {
|
||||
|
||||
export const workspaceQueryRunnerGraphqlApiExceptionHandler = (
|
||||
error: Error,
|
||||
context: WorkspaceSchemaBuilderContext,
|
||||
) => {
|
||||
if (error instanceof QueryFailedError) {
|
||||
if (
|
||||
error.message.includes('duplicate key value violates unique constraint')
|
||||
) {
|
||||
const indexNameMatch = error.message.match(/"([^"]+)"/);
|
||||
|
||||
if (indexNameMatch) {
|
||||
const indexName = indexNameMatch[1];
|
||||
|
||||
const deletedAtFieldMetadata = context.objectMetadataItem.fields.find(
|
||||
(field) => field.name === 'deletedAt',
|
||||
);
|
||||
|
||||
const affectedColumns = context.objectMetadataItem.indexMetadatas
|
||||
.find((index) => index.name === indexName)
|
||||
?.indexFieldMetadatas?.filter(
|
||||
(field) => field.fieldMetadataId !== deletedAtFieldMetadata?.id,
|
||||
)
|
||||
.map((indexField) => {
|
||||
const fieldMetadata = context.objectMetadataItem.fields.find(
|
||||
(objectField) => indexField.fieldMetadataId === objectField.id,
|
||||
);
|
||||
|
||||
return fieldMetadata?.label;
|
||||
});
|
||||
|
||||
const columnNames = affectedColumns?.join(', ');
|
||||
|
||||
if (affectedColumns?.length === 1) {
|
||||
throw new UserInputError(
|
||||
`Duplicate ${columnNames}. Please set a unique one.`,
|
||||
);
|
||||
}
|
||||
|
||||
throw new UserInputError(
|
||||
`A duplicate entry was detected. The combination of ${columnNames} must be unique.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (error instanceof WorkspaceQueryRunnerException) {
|
||||
switch (error.code) {
|
||||
case WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND:
|
||||
|
||||
@ -40,7 +40,7 @@ export class CreateManyResolverFactory
|
||||
|
||||
return await this.graphqlQueryRunnerService.createMany(args, options);
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error, context);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ export class CreateOneResolverFactory
|
||||
|
||||
return await this.graphqlQueryRunnerService.createOne(args, options);
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error, internalContext);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ export class DeleteManyResolverFactory
|
||||
|
||||
return await this.graphqlQueryRunnerService.deleteMany(args, options);
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error, internalContext);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ export class DeleteOneResolverFactory
|
||||
|
||||
return await this.graphqlQueryRunnerService.deleteOne(args, options);
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error, internalContext);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ export class DestroyManyResolverFactory
|
||||
|
||||
return await this.graphqlQueryRunnerService.destroyMany(args, options);
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error, internalContext);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ export class DestroyOneResolverFactory
|
||||
|
||||
return await this.graphQLQueryRunnerService.destroyOne(args, options);
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error, internalContext);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -43,7 +43,7 @@ export class FindDuplicatesResolverFactory
|
||||
options,
|
||||
);
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error, internalContext);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ export class FindManyResolverFactory
|
||||
|
||||
return await this.graphqlQueryRunnerService.findMany(args, options);
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error, internalContext);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ export class FindOneResolverFactory
|
||||
|
||||
return await this.graphqlQueryRunnerService.findOne(args, options);
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error, internalContext);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ export class RestoreManyResolverFactory
|
||||
|
||||
return await this.graphqlQueryRunnerService.restoreMany(args, options);
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error, internalContext);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ export class SearchResolverFactory
|
||||
|
||||
return await this.graphqlQueryRunnerService.search(args, options);
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error, internalContext);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ export class UpdateManyResolverFactory
|
||||
|
||||
return await this.graphqlQueryRunnerService.updateMany(args, options);
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error, internalContext);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ export class UpdateOneResolverFactory
|
||||
|
||||
return await this.graphqlQueryRunnerService.updateOne(args, options);
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error, internalContext);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -5,12 +5,12 @@ import { GraphQLOutputType } from 'graphql';
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { PageInfoType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/object';
|
||||
import {
|
||||
TypeMapperService,
|
||||
TypeOptions,
|
||||
} from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
|
||||
import { PageInfoType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/object';
|
||||
|
||||
import { ConnectionTypeDefinitionKind } from './connection-type-definition.factory';
|
||||
import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
|
||||
@ -27,7 +27,7 @@ export class ConnectionTypeFactory {
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
kind: ConnectionTypeDefinitionKind,
|
||||
buildOtions: WorkspaceBuildSchemaOptions,
|
||||
buildOptions: WorkspaceBuildSchemaOptions,
|
||||
typeOptions: TypeOptions,
|
||||
): GraphQLOutputType {
|
||||
if (kind === ConnectionTypeDefinitionKind.PageInfo) {
|
||||
@ -44,7 +44,7 @@ export class ConnectionTypeFactory {
|
||||
`Edge type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`,
|
||||
{
|
||||
objectMetadata,
|
||||
buildOtions,
|
||||
buildOptions,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -5,15 +5,15 @@ import { GraphQLOutputType } from 'graphql';
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { CursorScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import {
|
||||
TypeMapperService,
|
||||
TypeOptions,
|
||||
} from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
|
||||
import { CursorScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
|
||||
import { EdgeTypeDefinitionKind } from './edge-type-definition.factory';
|
||||
import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
|
||||
|
||||
@Injectable()
|
||||
export class EdgeTypeFactory {
|
||||
@ -27,7 +27,7 @@ export class EdgeTypeFactory {
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
kind: EdgeTypeDefinitionKind,
|
||||
buildOtions: WorkspaceBuildSchemaOptions,
|
||||
buildOptions: WorkspaceBuildSchemaOptions,
|
||||
typeOptions: TypeOptions,
|
||||
): GraphQLOutputType {
|
||||
if (kind === EdgeTypeDefinitionKind.Cursor) {
|
||||
@ -44,7 +44,7 @@ export class EdgeTypeFactory {
|
||||
`Node type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`,
|
||||
{
|
||||
objectMetadata,
|
||||
buildOtions,
|
||||
buildOptions,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -26,7 +26,7 @@ export class OutputTypeFactory {
|
||||
target: string,
|
||||
type: FieldMetadataType,
|
||||
kind: ObjectTypeDefinitionKind,
|
||||
buildOtions: WorkspaceBuildSchemaOptions,
|
||||
buildOptions: WorkspaceBuildSchemaOptions,
|
||||
typeOptions: TypeOptions,
|
||||
): GraphQLOutputType {
|
||||
let gqlType: GraphQLOutputType | undefined =
|
||||
@ -40,8 +40,9 @@ export class OutputTypeFactory {
|
||||
|
||||
if (!gqlType) {
|
||||
this.logger.error(`Could not find a GraphQL type for ${target}`, {
|
||||
kind,
|
||||
type,
|
||||
buildOtions,
|
||||
buildOptions,
|
||||
typeOptions,
|
||||
});
|
||||
|
||||
|
||||
@ -79,6 +79,7 @@ export class TypeMapperService {
|
||||
StringArrayScalarType as unknown as GraphQLScalarType,
|
||||
],
|
||||
[FieldMetadataType.RICH_TEXT, GraphQLString],
|
||||
[FieldMetadataType.TS_VECTOR, GraphQLString],
|
||||
]);
|
||||
|
||||
return typeScalarMapping.get(fieldMetadataType);
|
||||
@ -114,6 +115,7 @@ export class TypeMapperService {
|
||||
[FieldMetadataType.RAW_JSON, RawJsonFilterType],
|
||||
[FieldMetadataType.RICH_TEXT, StringFilterType],
|
||||
[FieldMetadataType.ARRAY, ArrayFilterType],
|
||||
[FieldMetadataType.TS_VECTOR, StringFilterType], // TODO: Add TSVectorFilterType
|
||||
]);
|
||||
|
||||
return typeFilterMapping.get(fieldMetadataType);
|
||||
@ -137,6 +139,7 @@ export class TypeMapperService {
|
||||
[FieldMetadataType.RAW_JSON, OrderByDirectionType],
|
||||
[FieldMetadataType.RICH_TEXT, OrderByDirectionType],
|
||||
[FieldMetadataType.ARRAY, OrderByDirectionType],
|
||||
[FieldMetadataType.TS_VECTOR, OrderByDirectionType], // TODO: Add TSVectorOrderByType
|
||||
]);
|
||||
|
||||
return typeOrderByMapping.get(fieldMetadataType);
|
||||
|
||||
@ -46,10 +46,7 @@ export const generateFields = <
|
||||
const fields = {};
|
||||
|
||||
for (const fieldMetadata of objectMetadata.fields) {
|
||||
if (
|
||||
isRelationFieldMetadataType(fieldMetadata.type) ||
|
||||
fieldMetadata.type === FieldMetadataType.TS_VECTOR
|
||||
) {
|
||||
if (isRelationFieldMetadataType(fieldMetadata.type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@ -76,6 +76,7 @@ export class WorkspaceSchemaFactory {
|
||||
(objectMetadataItem) => ({
|
||||
...objectMetadataItem,
|
||||
fields: Object.values(objectMetadataItem.fields),
|
||||
indexes: objectMetadataItem.indexMetadatas,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@ -31,9 +31,6 @@ describe('mapFieldMetadataToGraphqlQuery', () => {
|
||||
});
|
||||
describe('should handle all field metadata types', () => {
|
||||
Object.values(FieldMetadataType).forEach((fieldMetadataType) => {
|
||||
if (fieldMetadataType === FieldMetadataType.TS_VECTOR) {
|
||||
return;
|
||||
}
|
||||
it(`with field type ${fieldMetadataType}`, () => {
|
||||
const field = {
|
||||
type: fieldMetadataType,
|
||||
|
||||
@ -30,6 +30,7 @@ export const mapFieldMetadataToGraphqlQuery = (
|
||||
FieldMetadataType.RAW_JSON,
|
||||
FieldMetadataType.RICH_TEXT,
|
||||
FieldMetadataType.ARRAY,
|
||||
FieldMetadataType.TS_VECTOR,
|
||||
].includes(fieldType);
|
||||
|
||||
if (fieldIsSimpleValue) {
|
||||
|
||||
@ -13,6 +13,7 @@ const mockObjectMetadata: ObjectMetadataInterface = {
|
||||
fromRelations: [],
|
||||
toRelations: [],
|
||||
fields: [],
|
||||
indexMetadatas: [],
|
||||
isSystem: false,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
|
||||
@ -14,4 +14,5 @@ export enum FeatureFlagKey {
|
||||
IsWorkspaceMigratedForSearch = 'IS_WORKSPACE_MIGRATED_FOR_SEARCH',
|
||||
IsGmailSendEmailScopeEnabled = 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED',
|
||||
IsAnalyticsV2Enabled = 'IS_ANALYTICS_V2_ENABLED',
|
||||
IsUniqueIndexesEnabled = 'IS_UNIQUE_INDEXES_ENABLED',
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ export const emailsCompositeType: CompositeType = {
|
||||
type: FieldMetadataType.TEXT,
|
||||
hidden: false,
|
||||
isRequired: false,
|
||||
isIncludedInUniqueConstraint: true,
|
||||
},
|
||||
{
|
||||
name: 'additionalEmails',
|
||||
|
||||
@ -10,12 +10,14 @@ export const fullNameCompositeType: CompositeType = {
|
||||
type: FieldMetadataType.TEXT,
|
||||
hidden: false,
|
||||
isRequired: false,
|
||||
isIncludedInUniqueConstraint: true,
|
||||
},
|
||||
{
|
||||
name: 'lastName',
|
||||
type: FieldMetadataType.TEXT,
|
||||
hidden: false,
|
||||
isRequired: false,
|
||||
isIncludedInUniqueConstraint: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@ -16,6 +16,7 @@ export const linksCompositeType: CompositeType = {
|
||||
type: FieldMetadataType.TEXT,
|
||||
hidden: false,
|
||||
isRequired: false,
|
||||
isIncludedInUniqueConstraint: true,
|
||||
},
|
||||
{
|
||||
name: 'secondaryLinks',
|
||||
|
||||
@ -10,6 +10,7 @@ export const phonesCompositeType: CompositeType = {
|
||||
type: FieldMetadataType.TEXT,
|
||||
hidden: false,
|
||||
isRequired: false,
|
||||
isIncludedInUniqueConstraint: true,
|
||||
},
|
||||
{
|
||||
name: 'primaryPhoneCountryCode',
|
||||
|
||||
@ -118,6 +118,11 @@ export class FieldMetadataDTO<
|
||||
@Field({ nullable: true })
|
||||
isNullable?: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
isUnique?: boolean;
|
||||
|
||||
@Validate(IsFieldMetadataDefaultValue)
|
||||
@IsOptional()
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
|
||||
@ -108,6 +108,9 @@ export class FieldMetadataEntity<
|
||||
@Column({ nullable: true, default: true })
|
||||
isNullable: boolean;
|
||||
|
||||
@Column({ nullable: true, default: false })
|
||||
isUnique: boolean;
|
||||
|
||||
@Column({ nullable: false, type: 'uuid' })
|
||||
workspaceId: string;
|
||||
|
||||
@ -126,7 +129,7 @@ export class FieldMetadataEntity<
|
||||
@OneToMany(
|
||||
() => IndexFieldMetadataEntity,
|
||||
(indexFieldMetadata: IndexFieldMetadataEntity) =>
|
||||
indexFieldMetadata.fieldMetadata,
|
||||
indexFieldMetadata.indexMetadata,
|
||||
{
|
||||
cascade: true,
|
||||
},
|
||||
|
||||
@ -10,6 +10,7 @@ export interface CompositeProperty<
|
||||
type: Type;
|
||||
hidden: 'input' | 'output' | true | false;
|
||||
isRequired: boolean;
|
||||
isIncludedInUniqueConstraint?: boolean;
|
||||
isArray?: boolean;
|
||||
options?: FieldMetadataOptions<Type>;
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ export interface FieldMetadataInterface<
|
||||
workspaceId?: string;
|
||||
description?: string;
|
||||
isNullable?: boolean;
|
||||
isUnique?: boolean;
|
||||
fromRelationMetadata?: RelationMetadataEntity;
|
||||
toRelationMetadata?: RelationMetadataEntity;
|
||||
isCustom?: boolean;
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { IndexMetadataInterface } from 'src/engine/metadata-modules/index-metadata/interfaces/index-metadata.interface';
|
||||
|
||||
import { FieldMetadataInterface } from './field-metadata.interface';
|
||||
import { RelationMetadataInterface } from './relation-metadata.interface';
|
||||
|
||||
@ -13,6 +15,7 @@ export interface ObjectMetadataInterface {
|
||||
fromRelations: RelationMetadataInterface[];
|
||||
toRelations: RelationMetadataInterface[];
|
||||
fields: FieldMetadataInterface[];
|
||||
indexMetadatas: IndexMetadataInterface[];
|
||||
isSystem: boolean;
|
||||
isCustom: boolean;
|
||||
isActive: boolean;
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
import { Field, HideField, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
Authorize,
|
||||
FilterableField,
|
||||
IDField,
|
||||
QueryOptions,
|
||||
Relation,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
import { IsDateString, IsNotEmpty, IsNumber, IsUUID } from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
||||
|
||||
import { IndexMetadataDTO } from './index-metadata.dto';
|
||||
|
||||
@ObjectType('indexField')
|
||||
@Authorize({
|
||||
authorize: (context: any) => ({
|
||||
workspaceId: { eq: context?.req?.workspace?.id },
|
||||
}),
|
||||
})
|
||||
@QueryOptions({
|
||||
defaultResultSize: 10,
|
||||
disableSort: true,
|
||||
maxResultsSize: 1000,
|
||||
})
|
||||
@Relation('indexMetadata', () => IndexMetadataDTO, {
|
||||
nullable: true,
|
||||
})
|
||||
@Relation('fieldMetadata', () => FieldMetadataDTO, {
|
||||
nullable: true,
|
||||
})
|
||||
export class IndexFieldMetadataDTO {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@IDField(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
indexMetadataId: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@FilterableField(() => UUIDScalarType)
|
||||
fieldMetadataId: string;
|
||||
|
||||
@IsNumber()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
order: number;
|
||||
|
||||
@IsDateString()
|
||||
@Field()
|
||||
createdAt: Date;
|
||||
|
||||
@IsDateString()
|
||||
@Field()
|
||||
updatedAt: Date;
|
||||
|
||||
@HideField()
|
||||
workspaceId: string;
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
import {
|
||||
Field,
|
||||
HideField,
|
||||
ObjectType,
|
||||
registerEnumType,
|
||||
} from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
Authorize,
|
||||
CursorConnection,
|
||||
FilterableField,
|
||||
IDField,
|
||||
QueryOptions,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsDateString,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator';
|
||||
import { IndexFieldMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-field-metadata.dto';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
|
||||
|
||||
registerEnumType(IndexType, {
|
||||
name: 'IndexType',
|
||||
description: 'Type of the index',
|
||||
});
|
||||
|
||||
@ObjectType('index')
|
||||
@Authorize({
|
||||
authorize: (context: any) => ({
|
||||
workspaceId: { eq: context?.req?.workspace?.id },
|
||||
}),
|
||||
})
|
||||
@QueryOptions({
|
||||
defaultResultSize: 10,
|
||||
disableSort: true,
|
||||
maxResultsSize: 1000,
|
||||
})
|
||||
@CursorConnection('objectMetadata', () => ObjectMetadataDTO)
|
||||
@CursorConnection('indexFieldMetadatas', () => IndexFieldMetadataDTO)
|
||||
export class IndexMetadataDTO {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@IDField(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
@IsValidMetadataName()
|
||||
name: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@FilterableField({ nullable: true })
|
||||
isCustom?: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
isUnique: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
indexWhereClause?: string;
|
||||
|
||||
@IsEnum(IndexType)
|
||||
@IsNotEmpty()
|
||||
@Field(() => IndexType)
|
||||
indexType: IndexType;
|
||||
|
||||
objectMetadataId: string;
|
||||
|
||||
@IsDateString()
|
||||
@Field()
|
||||
createdAt: Date;
|
||||
|
||||
@IsDateString()
|
||||
@Field()
|
||||
updatedAt: Date;
|
||||
|
||||
@HideField()
|
||||
workspaceId: string;
|
||||
}
|
||||
@ -23,6 +23,12 @@ export class IndexMetadataEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
|
||||
@Column({ nullable: false })
|
||||
name: string;
|
||||
|
||||
@ -32,7 +38,7 @@ export class IndexMetadataEntity {
|
||||
@Column({ nullable: false, type: 'uuid' })
|
||||
objectMetadataId: string;
|
||||
|
||||
@ManyToOne(() => ObjectMetadataEntity, (object) => object.indexes, {
|
||||
@ManyToOne(() => ObjectMetadataEntity, (object) => object.indexMetadatas, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
@ -48,15 +54,15 @@ export class IndexMetadataEntity {
|
||||
)
|
||||
indexFieldMetadatas: Relation<IndexFieldMetadataEntity[]>;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
|
||||
@Column({ default: false })
|
||||
isCustom: boolean;
|
||||
|
||||
@Column({ nullable: false, default: false })
|
||||
isUnique: boolean;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
indexWhereClause: string | null;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: IndexType,
|
||||
|
||||
@ -1,14 +1,50 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { SortDirection } from '@ptc-org/nestjs-query-core';
|
||||
import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
|
||||
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
|
||||
import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service';
|
||||
import { ObjectMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/object-metadata/interceptors/object-metadata-graphql-api-exception.interceptor';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([IndexMetadataEntity], 'metadata'),
|
||||
WorkspaceMigrationModule,
|
||||
NestjsQueryGraphQLModule.forFeature({
|
||||
imports: [
|
||||
NestjsQueryTypeOrmModule.forFeature(
|
||||
[IndexMetadataEntity, IndexFieldMetadataEntity],
|
||||
'metadata',
|
||||
),
|
||||
WorkspaceMigrationModule,
|
||||
],
|
||||
services: [IndexMetadataService],
|
||||
resolvers: [
|
||||
{
|
||||
EntityClass: IndexMetadataEntity,
|
||||
DTOClass: IndexMetadataDTO,
|
||||
read: {
|
||||
defaultSort: [{ field: 'id', direction: SortDirection.DESC }],
|
||||
many: {
|
||||
name: 'indexMetadatas', //TODO: check + singular
|
||||
},
|
||||
},
|
||||
create: {
|
||||
disabled: true,
|
||||
},
|
||||
update: { disabled: true },
|
||||
delete: { disabled: true },
|
||||
guards: [WorkspaceAuthGuard],
|
||||
interceptors: [ObjectMetadataGraphqlApiExceptionInterceptor],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
providers: [IndexMetadataService],
|
||||
exports: [IndexMetadataService],
|
||||
|
||||
@ -32,8 +32,10 @@ export class IndexMetadataService {
|
||||
workspaceId: string,
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
fieldMetadataToIndex: Partial<FieldMetadataEntity>[],
|
||||
isUnique: boolean,
|
||||
isCustom: boolean,
|
||||
indexType?: IndexType,
|
||||
indexWhereClause?: string,
|
||||
) {
|
||||
const tableName = computeObjectTargetTable(objectMetadata);
|
||||
|
||||
@ -82,6 +84,8 @@ export class IndexMetadataService {
|
||||
action: WorkspaceMigrationIndexActionType.CREATE,
|
||||
columns: columnNames,
|
||||
name: indexName,
|
||||
isUnique,
|
||||
where: indexWhereClause,
|
||||
type: indexType,
|
||||
},
|
||||
],
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
import { IndexMetadataInterface } from 'src/engine/metadata-modules/index-metadata/interfaces/index-metadata.interface';
|
||||
|
||||
export interface IndexFieldMetadataInterface {
|
||||
id: string;
|
||||
indexMetadataId: string;
|
||||
fieldMetadataId: string;
|
||||
fieldMetadata: FieldMetadataInterface;
|
||||
indexMetadata: IndexMetadataInterface;
|
||||
order: number;
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
import { IndexFieldMetadataInterface } from 'src/engine/metadata-modules/index-metadata/interfaces/index-field-metadata.interface';
|
||||
|
||||
export interface IndexMetadataInterface {
|
||||
name: string;
|
||||
isUnique: boolean;
|
||||
indexFieldMetadatas: IndexFieldMetadataInterface[];
|
||||
}
|
||||
@ -11,6 +11,7 @@ import {
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
||||
import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
|
||||
import { BeforeDeleteOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-delete-one-object.hook';
|
||||
|
||||
@ObjectType('object')
|
||||
@ -26,6 +27,7 @@ import { BeforeDeleteOneObject } from 'src/engine/metadata-modules/object-metada
|
||||
})
|
||||
@BeforeDeleteOne(BeforeDeleteOneObject)
|
||||
@CursorConnection('fields', () => FieldMetadataDTO)
|
||||
@CursorConnection('indexMetadatas', () => IndexMetadataDTO)
|
||||
export class ObjectMetadataDTO {
|
||||
@IDField(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@ -86,7 +86,7 @@ export class ObjectMetadataEntity implements ObjectMetadataInterface {
|
||||
@OneToMany(() => IndexMetadataEntity, (index) => index.objectMetadata, {
|
||||
cascade: true,
|
||||
})
|
||||
indexes: Relation<IndexMetadataEntity[]>;
|
||||
indexMetadatas: Relation<IndexMetadataEntity[]>;
|
||||
|
||||
@OneToMany(
|
||||
() => RelationMetadataEntity,
|
||||
|
||||
@ -673,6 +673,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
createdObjectMetadata,
|
||||
[searchVectorFieldMetadata],
|
||||
false,
|
||||
false,
|
||||
IndexType.GIN,
|
||||
);
|
||||
}
|
||||
|
||||
@ -154,6 +154,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
|
||||
toObjectMetadata,
|
||||
[foreignKeyFieldMetadata, deletedFieldMetadata],
|
||||
false,
|
||||
false,
|
||||
);
|
||||
|
||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
||||
|
||||
@ -77,6 +77,8 @@ export class WorkspaceMetadataCacheService {
|
||||
'fields',
|
||||
'fields.fromRelationMetadata',
|
||||
'fields.toRelationMetadata',
|
||||
'indexMetadatas',
|
||||
'indexMetadatas.indexFieldMetadatas',
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@ -49,6 +49,7 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicF
|
||||
columnType: fieldMetadataTypeToColumnType(fieldMetadata.type),
|
||||
isArray: fieldMetadata.type === FieldMetadataType.ARRAY,
|
||||
isNullable: fieldMetadata.isNullable ?? true,
|
||||
isUnique: fieldMetadata.isUnique ?? false,
|
||||
defaultValue: serializedDefaultValue,
|
||||
},
|
||||
];
|
||||
@ -83,6 +84,7 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicF
|
||||
columnType: fieldMetadataTypeToColumnType(currentFieldMetadata.type),
|
||||
isArray: currentFieldMetadata.type === FieldMetadataType.ARRAY,
|
||||
isNullable: currentFieldMetadata.isNullable ?? true,
|
||||
isUnique: currentFieldMetadata.isUnique ?? false,
|
||||
defaultValue: serializeDefaultValue(
|
||||
currentFieldMetadata.defaultValue,
|
||||
),
|
||||
@ -92,6 +94,7 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicF
|
||||
columnType: fieldMetadataTypeToColumnType(alteredFieldMetadata.type),
|
||||
isArray: alteredFieldMetadata.type === FieldMetadataType.ARRAY,
|
||||
isNullable: alteredFieldMetadata.isNullable ?? true,
|
||||
isUnique: alteredFieldMetadata.isUnique ?? false,
|
||||
defaultValue: serializedDefaultValue,
|
||||
},
|
||||
},
|
||||
|
||||
@ -69,6 +69,7 @@ export class CompositeColumnActionFactory extends ColumnActionAbstractFactory<Co
|
||||
columnType: fieldMetadataTypeToColumnType(property.type),
|
||||
enum: enumOptions,
|
||||
isNullable: fieldMetadata.isNullable || !property.isRequired,
|
||||
isUnique: fieldMetadata.isUnique,
|
||||
defaultValue: serializedDefaultValue,
|
||||
isArray:
|
||||
property.type === FieldMetadataType.MULTI_SELECT || property.isArray,
|
||||
@ -168,6 +169,7 @@ export class CompositeColumnActionFactory extends ColumnActionAbstractFactory<Co
|
||||
: undefined,
|
||||
isNullable:
|
||||
currentFieldMetadata.isNullable || !currentProperty.isRequired,
|
||||
isUnique: currentFieldMetadata.isUnique ?? false,
|
||||
defaultValue: serializeDefaultValue(
|
||||
currentFieldMetadata.defaultValue?.[currentProperty.name],
|
||||
),
|
||||
@ -181,6 +183,7 @@ export class CompositeColumnActionFactory extends ColumnActionAbstractFactory<Co
|
||||
enum: enumOptions,
|
||||
isNullable:
|
||||
alteredFieldMetadata.isNullable || !alteredProperty.isRequired,
|
||||
isUnique: alteredFieldMetadata.isUnique ?? false,
|
||||
defaultValue: serializedDefaultValue,
|
||||
isArray:
|
||||
alteredProperty.type === FieldMetadataType.MULTI_SELECT ||
|
||||
|
||||
@ -46,6 +46,7 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory<EnumFie
|
||||
enum: enumOptions,
|
||||
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
|
||||
isNullable: fieldMetadata.isNullable ?? true,
|
||||
isUnique: fieldMetadata.isUnique ?? false,
|
||||
defaultValue: serializedDefaultValue,
|
||||
},
|
||||
];
|
||||
@ -103,6 +104,7 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory<EnumFie
|
||||
: undefined,
|
||||
isArray: currentFieldMetadata.type === FieldMetadataType.MULTI_SELECT,
|
||||
isNullable: currentFieldMetadata.isNullable ?? true,
|
||||
isUnique: currentFieldMetadata.isUnique ?? false,
|
||||
defaultValue: serializeDefaultValue(
|
||||
currentFieldMetadata.defaultValue,
|
||||
),
|
||||
@ -113,6 +115,7 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory<EnumFie
|
||||
enum: enumOptions,
|
||||
isArray: alteredFieldMetadata.type === FieldMetadataType.MULTI_SELECT,
|
||||
isNullable: alteredFieldMetadata.isNullable ?? true,
|
||||
isUnique: alteredFieldMetadata.isUnique ?? false,
|
||||
defaultValue: serializedDefaultValue,
|
||||
},
|
||||
},
|
||||
|
||||
@ -32,6 +32,7 @@ export class TsVectorColumnActionFactory extends ColumnActionAbstractFactory<TsV
|
||||
columnName: computeColumnName(fieldMetadata),
|
||||
columnType: fieldMetadataTypeToColumnType(fieldMetadata.type),
|
||||
isNullable: fieldMetadata.isNullable ?? true,
|
||||
isUnique: fieldMetadata.isUnique ?? false,
|
||||
defaultValue: undefined,
|
||||
generatedType: fieldMetadata.generatedType,
|
||||
asExpression: fieldMetadata.asExpression,
|
||||
|
||||
@ -30,6 +30,7 @@ export interface WorkspaceMigrationColumnDefinition {
|
||||
enum?: WorkspaceMigrationEnum[];
|
||||
isArray?: boolean;
|
||||
isNullable: boolean;
|
||||
isUnique?: boolean;
|
||||
defaultValue: any;
|
||||
generatedType?: 'STORED' | 'VIRTUAL';
|
||||
asExpression?: string;
|
||||
@ -39,6 +40,8 @@ export interface WorkspaceMigrationIndexAction {
|
||||
action: WorkspaceMigrationIndexActionType;
|
||||
name: string;
|
||||
columns: string[];
|
||||
isUnique: boolean;
|
||||
where?: string | null;
|
||||
type?: IndexType;
|
||||
}
|
||||
|
||||
|
||||
@ -12,8 +12,8 @@ import {
|
||||
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceCustomEntity } from 'src/engine/twenty-orm/decorators/workspace-custom-entity.decorator';
|
||||
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.decorator';
|
||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||
@ -157,6 +157,6 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
@WorkspaceIsSystem()
|
||||
@WorkspaceIndex({ indexType: IndexType.GIN })
|
||||
@WorkspaceFieldIndex({ indexType: IndexType.GIN })
|
||||
[SEARCH_VECTOR_FIELD.name]: any;
|
||||
}
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name';
|
||||
import { WorkspaceIndexOptions } from 'src/engine/twenty-orm/decorators/workspace-index.decorator';
|
||||
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
|
||||
import { getColumnsForIndex } from 'src/engine/twenty-orm/utils/get-default-columns-for-index.util';
|
||||
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
|
||||
import { TypedReflect } from 'src/utils/typed-reflect';
|
||||
|
||||
export function WorkspaceFieldIndex(
|
||||
options?: WorkspaceIndexOptions,
|
||||
): PropertyDecorator {
|
||||
return (target: any, propertyKey: string | symbol) => {
|
||||
if (propertyKey === undefined) {
|
||||
throw new Error('This decorator should be used with a field not a class');
|
||||
}
|
||||
|
||||
const gate = TypedReflect.getMetadata(
|
||||
'workspace:gate-metadata-args',
|
||||
target,
|
||||
propertyKey.toString(),
|
||||
);
|
||||
|
||||
const additionalDefaultColumnsForIndex = getColumnsForIndex(
|
||||
options?.indexType,
|
||||
);
|
||||
|
||||
const columns = [
|
||||
propertyKey.toString(),
|
||||
...additionalDefaultColumnsForIndex,
|
||||
];
|
||||
|
||||
metadataArgsStorage.addIndexes({
|
||||
name: `IDX_${generateDeterministicIndexName([
|
||||
convertClassNameToObjectMetadataName(target.constructor.name),
|
||||
...columns,
|
||||
])}`,
|
||||
columns,
|
||||
target: target.constructor,
|
||||
gate,
|
||||
isUnique: options?.isUnique ?? false,
|
||||
whereClause: options?.indexWhereClause ?? null,
|
||||
type: options?.indexType,
|
||||
});
|
||||
};
|
||||
}
|
||||
@ -57,6 +57,12 @@ export function WorkspaceField<T extends FieldMetadataType>(
|
||||
object,
|
||||
propertyKey.toString(),
|
||||
) ?? false;
|
||||
const isUnique =
|
||||
TypedReflect.getMetadata(
|
||||
'workspace:is-unique-metadata-args',
|
||||
object,
|
||||
propertyKey.toString(),
|
||||
) ?? false;
|
||||
|
||||
const defaultValue = (options.defaultValue ??
|
||||
generateDefaultValue(options.type)) as FieldMetadataDefaultValue | null;
|
||||
@ -77,6 +83,7 @@ export function WorkspaceField<T extends FieldMetadataType>(
|
||||
isSystem,
|
||||
gate,
|
||||
isDeprecated,
|
||||
isUnique,
|
||||
isActive: options.isActive,
|
||||
asExpression: options.asExpression,
|
||||
generatedType: options.generatedType,
|
||||
|
||||
@ -1,83 +1,40 @@
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name';
|
||||
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
|
||||
import { getColumnsForIndex } from 'src/engine/twenty-orm/utils/get-default-columns-for-index.util';
|
||||
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
|
||||
import { isDefined } from 'src/utils/is-defined';
|
||||
import { TypedReflect } from 'src/utils/typed-reflect';
|
||||
|
||||
export type WorkspaceIndexMetadata = {
|
||||
columns?: string[];
|
||||
export type WorkspaceIndexOptions = {
|
||||
isUnique?: boolean;
|
||||
indexWhereClause?: string;
|
||||
indexType?: IndexType;
|
||||
};
|
||||
|
||||
export function WorkspaceIndex(
|
||||
metadata?: WorkspaceIndexMetadata,
|
||||
): PropertyDecorator;
|
||||
export function WorkspaceIndex(
|
||||
metadata: WorkspaceIndexMetadata,
|
||||
): ClassDecorator;
|
||||
export function WorkspaceIndex(
|
||||
metadata?: WorkspaceIndexMetadata,
|
||||
): PropertyDecorator | ClassDecorator {
|
||||
return (target: any, propertyKey: string | symbol) => {
|
||||
if (propertyKey === undefined && metadata === undefined) {
|
||||
throw new Error('Class level WorkspaceIndex should be used with columns');
|
||||
}
|
||||
|
||||
if (propertyKey !== undefined && metadata?.columns !== undefined) {
|
||||
throw new Error(
|
||||
'Property level WorkspaceIndex should not be used with columns',
|
||||
);
|
||||
}
|
||||
columns: string[],
|
||||
options: WorkspaceIndexOptions,
|
||||
): ClassDecorator {
|
||||
if (!Array.isArray(columns) || columns.length === 0) {
|
||||
throw new Error('Class level WorkspaceIndex should be used with columns');
|
||||
}
|
||||
|
||||
return (target: any) => {
|
||||
const gate = TypedReflect.getMetadata(
|
||||
'workspace:gate-metadata-args',
|
||||
target,
|
||||
propertyKey.toString(),
|
||||
);
|
||||
|
||||
// TODO: handle composite field metadata types
|
||||
if (isDefined(metadata?.columns)) {
|
||||
const columns = metadata.columns;
|
||||
|
||||
if (columns.length > 0) {
|
||||
metadataArgsStorage.addIndexes({
|
||||
name: `IDX_${generateDeterministicIndexName([
|
||||
convertClassNameToObjectMetadataName(target.name),
|
||||
...columns,
|
||||
])}`,
|
||||
columns,
|
||||
target: target,
|
||||
gate,
|
||||
...(isDefined(metadata?.indexType)
|
||||
? { type: metadata.indexType }
|
||||
: {}),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isDefined(propertyKey)) {
|
||||
const additionalDefaultColumnsForIndex = getColumnsForIndex(
|
||||
metadata?.indexType,
|
||||
);
|
||||
const columns = [
|
||||
propertyKey.toString(),
|
||||
...additionalDefaultColumnsForIndex,
|
||||
];
|
||||
|
||||
metadataArgsStorage.addIndexes({
|
||||
name: `IDX_${generateDeterministicIndexName([
|
||||
convertClassNameToObjectMetadataName(target.constructor.name),
|
||||
...columns,
|
||||
])}`,
|
||||
columns,
|
||||
target: target.constructor,
|
||||
...(isDefined(metadata?.indexType) ? { type: metadata.indexType } : {}),
|
||||
gate,
|
||||
});
|
||||
}
|
||||
metadataArgsStorage.addIndexes({
|
||||
name: `IDX_${generateDeterministicIndexName([
|
||||
convertClassNameToObjectMetadataName(target.name),
|
||||
...columns,
|
||||
])}`,
|
||||
columns,
|
||||
target: target,
|
||||
gate,
|
||||
isUnique: options?.isUnique ?? false,
|
||||
whereClause: options?.indexWhereClause ?? null,
|
||||
type: options?.indexType,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name';
|
||||
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
|
||||
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
|
||||
import { TypedReflect } from 'src/utils/typed-reflect';
|
||||
|
||||
export function WorkspaceIsUnique(): PropertyDecorator {
|
||||
return (target: any, propertyKey: string | symbol) => {
|
||||
if (propertyKey === undefined) {
|
||||
throw new Error('This decorator should be used with a field not a class');
|
||||
}
|
||||
|
||||
const gate = TypedReflect.getMetadata(
|
||||
'workspace:gate-metadata-args',
|
||||
target,
|
||||
propertyKey.toString(),
|
||||
);
|
||||
|
||||
const columns = [propertyKey.toString()];
|
||||
|
||||
metadataArgsStorage.addIndexes({
|
||||
name: `IDX_UNIQUE_${generateDeterministicIndexName([
|
||||
convertClassNameToObjectMetadataName(target.constructor.name),
|
||||
...columns,
|
||||
])}`,
|
||||
columns,
|
||||
target: target.constructor,
|
||||
gate,
|
||||
isUnique: true,
|
||||
whereClause: null,
|
||||
});
|
||||
|
||||
return TypedReflect.defineMetadata(
|
||||
'workspace:is-unique-metadata-args',
|
||||
true,
|
||||
target,
|
||||
propertyKey.toString(),
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.decorator';
|
||||
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
|
||||
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
|
||||
|
||||
export function WorkspaceJoinColumn(
|
||||
@ -12,6 +12,6 @@ export function WorkspaceJoinColumn(
|
||||
});
|
||||
|
||||
// Register index for join column
|
||||
WorkspaceIndex()(object, propertyKey);
|
||||
WorkspaceFieldIndex()(object, propertyKey);
|
||||
};
|
||||
}
|
||||
|
||||
@ -75,6 +75,11 @@ export interface WorkspaceFieldMetadataArgs {
|
||||
*/
|
||||
readonly isNullable: boolean;
|
||||
|
||||
/**
|
||||
* Is unique field.
|
||||
*/
|
||||
readonly isUnique: boolean;
|
||||
|
||||
/**
|
||||
* Field gate.
|
||||
*/
|
||||
|
||||
@ -19,11 +19,21 @@ export interface WorkspaceIndexMetadataArgs {
|
||||
*/
|
||||
columns: string[];
|
||||
|
||||
/**
|
||||
* Is index unique.
|
||||
*/
|
||||
isUnique: boolean;
|
||||
|
||||
/*
|
||||
* Index type. Defaults to Btree.
|
||||
*/
|
||||
type?: IndexType;
|
||||
|
||||
/**
|
||||
* Index where clause.
|
||||
*/
|
||||
whereClause: string | null;
|
||||
|
||||
/**
|
||||
* Field gate.
|
||||
*/
|
||||
|
||||
@ -5,9 +5,9 @@ export interface WorkspaceTableStructure {
|
||||
dataType: string;
|
||||
columnDefault: string;
|
||||
isNullable: boolean;
|
||||
isUnique: boolean;
|
||||
isPrimaryKey: boolean;
|
||||
isForeignKey: boolean;
|
||||
isUnique: boolean;
|
||||
isArray: boolean;
|
||||
onUpdateAction: string;
|
||||
onDeleteAction: string;
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
|
||||
import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface';
|
||||
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
||||
@ -77,10 +81,8 @@ export class WorkspaceMigrationIndexFactory {
|
||||
objectMetadata.fields.map((field) => [field.id, field]),
|
||||
);
|
||||
|
||||
const indexes = indexMetadataCollection.map((indexMetadata) => ({
|
||||
name: indexMetadata.name,
|
||||
action: WorkspaceMigrationIndexActionType.CREATE,
|
||||
columns: indexMetadata.indexFieldMetadatas
|
||||
const indexes = indexMetadataCollection.map((indexMetadata) => {
|
||||
const columns = indexMetadata.indexFieldMetadatas
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.map((indexFieldMetadata) => {
|
||||
const fieldMetadata =
|
||||
@ -92,10 +94,35 @@ export class WorkspaceMigrationIndexFactory {
|
||||
);
|
||||
}
|
||||
|
||||
return fieldMetadata.name;
|
||||
}),
|
||||
type: indexMetadata.indexType,
|
||||
}));
|
||||
if (!isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
return fieldMetadata.name;
|
||||
}
|
||||
|
||||
const compositeType = compositeTypeDefinitions.get(
|
||||
fieldMetadata.type,
|
||||
) as CompositeType;
|
||||
|
||||
return compositeType.properties
|
||||
.filter((property) => property.isIncludedInUniqueConstraint)
|
||||
.map((property) =>
|
||||
computeCompositeColumnName(fieldMetadata, property),
|
||||
);
|
||||
})
|
||||
.flat();
|
||||
|
||||
const defaultWhereClause = indexMetadata.isUnique
|
||||
? `${columns.map((column) => `"${column}"`).join(" != '' AND ")} != '' AND "deletedAt" IS NULL`
|
||||
: null;
|
||||
|
||||
return {
|
||||
name: indexMetadata.name,
|
||||
action: WorkspaceMigrationIndexActionType.CREATE,
|
||||
isUnique: indexMetadata.isUnique,
|
||||
columns,
|
||||
type: indexMetadata.indexType,
|
||||
where: indexMetadata.indexWhereClause ?? defaultWhereClause,
|
||||
};
|
||||
});
|
||||
|
||||
workspaceMigrations.push({
|
||||
workspaceId: objectMetadata.workspaceId,
|
||||
@ -134,6 +161,7 @@ export class WorkspaceMigrationIndexFactory {
|
||||
name: indexMetadata.name,
|
||||
action: WorkspaceMigrationIndexActionType.DROP,
|
||||
columns: [],
|
||||
isUnique: indexMetadata.isUnique,
|
||||
}));
|
||||
|
||||
workspaceMigrations.push({
|
||||
|
||||
@ -77,6 +77,7 @@ export class WorkspaceMigrationEnumService {
|
||||
enumName: newEnumTypeName,
|
||||
isArray: columnDefinition.isArray,
|
||||
isNullable: columnDefinition.isNullable,
|
||||
isUnique: columnDefinition.isUnique,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'class-validator';
|
||||
import {
|
||||
QueryRunner,
|
||||
Table,
|
||||
@ -9,6 +10,7 @@ import {
|
||||
TableUnique,
|
||||
} from 'typeorm';
|
||||
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import {
|
||||
WorkspaceMigrationColumnAction,
|
||||
WorkspaceMigrationColumnActionType,
|
||||
@ -27,7 +29,6 @@ import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/work
|
||||
import { WorkspaceMigrationEnumService } from 'src/engine/workspace-manager/workspace-migration-runner/services/workspace-migration-enum.service';
|
||||
import { convertOnDeleteActionToOnDelete } from 'src/engine/workspace-manager/workspace-migration-runner/utils/convert-on-delete-action-to-on-delete.util';
|
||||
import { tableDefaultColumns } from 'src/engine/workspace-manager/workspace-migration-runner/utils/table-default-column.util';
|
||||
import { isDefined } from 'src/utils/is-defined';
|
||||
|
||||
import { WorkspaceMigrationTypeService } from './services/workspace-migration-type.service';
|
||||
|
||||
@ -200,7 +201,7 @@ export class WorkspaceMigrationRunnerService {
|
||||
for (const index of indexes) {
|
||||
switch (index.action) {
|
||||
case WorkspaceMigrationIndexActionType.CREATE:
|
||||
if (isDefined(index.type)) {
|
||||
if (isDefined(index.type) && index.type !== IndexType.BTREE) {
|
||||
const quotedColumns = index.columns.map((column) => `"${column}"`);
|
||||
|
||||
await queryRunner.query(`
|
||||
@ -212,6 +213,8 @@ export class WorkspaceMigrationRunnerService {
|
||||
new TableIndex({
|
||||
name: index.name,
|
||||
columnNames: index.columns,
|
||||
isUnique: index.isUnique,
|
||||
where: index.where ?? undefined,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@ -404,6 +407,7 @@ export class WorkspaceMigrationRunnerService {
|
||||
enumName: enumName,
|
||||
isArray: migrationColumn.isArray,
|
||||
isNullable: migrationColumn.isNullable,
|
||||
isUnique: migrationColumn.isUnique,
|
||||
asExpression: migrationColumn.asExpression,
|
||||
generatedType: migrationColumn.generatedType,
|
||||
}),
|
||||
@ -459,6 +463,7 @@ export class WorkspaceMigrationRunnerService {
|
||||
),
|
||||
isArray: migrationColumn.currentColumnDefinition.isArray,
|
||||
isNullable: migrationColumn.currentColumnDefinition.isNullable,
|
||||
isUnique: migrationColumn.currentColumnDefinition.isUnique,
|
||||
}),
|
||||
new TableColumn({
|
||||
name: migrationColumn.alteredColumnDefinition.columnName,
|
||||
@ -469,6 +474,7 @@ export class WorkspaceMigrationRunnerService {
|
||||
),
|
||||
isArray: migrationColumn.alteredColumnDefinition.isArray,
|
||||
isNullable: migrationColumn.alteredColumnDefinition.isNullable,
|
||||
isUnique: migrationColumn.alteredColumnDefinition.isUnique,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,8 +3,8 @@ import { Injectable } from '@nestjs/common';
|
||||
import diff from 'microdiff';
|
||||
|
||||
import {
|
||||
IndexComparatorResult,
|
||||
ComparatorAction,
|
||||
IndexComparatorResult,
|
||||
} from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface';
|
||||
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import diff from 'microdiff';
|
||||
import omit from 'lodash.omit';
|
||||
import diff from 'microdiff';
|
||||
|
||||
import {
|
||||
ComparatorAction,
|
||||
@ -9,8 +9,8 @@ import {
|
||||
} from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface';
|
||||
import { ComputedPartialWorkspaceEntity } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-object-metadata.interface';
|
||||
|
||||
import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
|
||||
|
||||
const objectPropertiesToIgnore = [
|
||||
'id',
|
||||
@ -28,7 +28,10 @@ export class WorkspaceObjectComparator {
|
||||
|
||||
public compare(
|
||||
originalObjectMetadata: Omit<ObjectMetadataEntity, 'fields'> | undefined,
|
||||
standardObjectMetadata: Omit<ComputedPartialWorkspaceEntity, 'fields'>,
|
||||
standardObjectMetadata: Omit<
|
||||
ComputedPartialWorkspaceEntity,
|
||||
'fields' | 'indexMetadatas'
|
||||
>,
|
||||
): ObjectComparatorResult {
|
||||
// If the object doesn't exist in the original metadata, we need to create it
|
||||
if (!originalObjectMetadata) {
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
|
||||
import { getJoinColumn } from 'src/engine/twenty-orm/utils/get-join-column.util';
|
||||
@ -163,6 +164,7 @@ export class StandardFieldFactory {
|
||||
settings: workspaceFieldMetadataArgs.settings,
|
||||
workspaceId: context.workspaceId,
|
||||
isNullable: workspaceFieldMetadataArgs.isNullable,
|
||||
isUnique: workspaceFieldMetadataArgs.isUnique,
|
||||
isCustom: workspaceFieldMetadataArgs.isDeprecated ? true : false,
|
||||
isSystem: workspaceFieldMetadataArgs.isSystem ?? false,
|
||||
isActive: workspaceFieldMetadataArgs.isActive ?? true,
|
||||
@ -218,6 +220,9 @@ export class StandardFieldFactory {
|
||||
isCustom: false,
|
||||
isSystem: true,
|
||||
isNullable: workspaceRelationMetadataArgs.isNullable,
|
||||
isUnique:
|
||||
workspaceRelationMetadataArgs.type ===
|
||||
RelationMetadataType.ONE_TO_ONE,
|
||||
isActive: workspaceRelationMetadataArgs.isActive ?? true,
|
||||
});
|
||||
}
|
||||
@ -236,6 +241,8 @@ export class StandardFieldFactory {
|
||||
workspaceEntityMetadataArgs?.isSystem ||
|
||||
workspaceRelationMetadataArgs.isSystem,
|
||||
isNullable: true,
|
||||
isUnique:
|
||||
workspaceRelationMetadataArgs.type === RelationMetadataType.ONE_TO_ONE,
|
||||
isActive: workspaceRelationMetadataArgs.isActive ?? true,
|
||||
});
|
||||
|
||||
|
||||
@ -88,7 +88,9 @@ export class StandardIndexFactory {
|
||||
objectMetadataId: objectMetadata.id,
|
||||
name: workspaceIndexMetadataArgs.name,
|
||||
columns: workspaceIndexMetadataArgs.columns,
|
||||
isUnique: workspaceIndexMetadataArgs.isUnique,
|
||||
isCustom: false,
|
||||
indexWhereClause: workspaceIndexMetadataArgs.whereClause,
|
||||
indexType: workspaceIndexMetadataArgs.type,
|
||||
};
|
||||
|
||||
@ -130,7 +132,9 @@ export class StandardIndexFactory {
|
||||
name: `IDX_${generateDeterministicIndexName([computeTableName(customObjectName, true), ...workspaceIndexMetadataArgs.columns])}`,
|
||||
columns: workspaceIndexMetadataArgs.columns,
|
||||
isCustom: false,
|
||||
isUnique: workspaceIndexMetadataArgs.isUnique,
|
||||
indexType: workspaceIndexMetadataArgs.type,
|
||||
indexWhereClause: workspaceIndexMetadataArgs.whereClause,
|
||||
};
|
||||
|
||||
return indexMetadata;
|
||||
|
||||
@ -14,7 +14,7 @@ export class StandardObjectFactory {
|
||||
standardObjectMetadataDefinitions: (typeof BaseWorkspaceEntity)[],
|
||||
context: WorkspaceSyncContext,
|
||||
workspaceFeatureFlagsMap: FeatureFlagMap,
|
||||
): Omit<PartialWorkspaceEntity, 'fields'>[] {
|
||||
): Omit<PartialWorkspaceEntity, 'fields' | 'indexMetadatas'>[] {
|
||||
return standardObjectMetadataDefinitions
|
||||
.map((metadata) =>
|
||||
this.createObjectMetadata(metadata, context, workspaceFeatureFlagsMap),
|
||||
@ -26,7 +26,7 @@ export class StandardObjectFactory {
|
||||
target: typeof BaseWorkspaceEntity,
|
||||
context: WorkspaceSyncContext,
|
||||
workspaceFeatureFlagsMap: FeatureFlagMap,
|
||||
): Omit<PartialWorkspaceEntity, 'fields'> | undefined {
|
||||
): Omit<PartialWorkspaceEntity, 'fields' | 'indexMetadatas'> | undefined {
|
||||
const workspaceEntityMetadataArgs =
|
||||
metadataArgsStorage.filterEntities(target);
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
|
||||
import { ComputedPartialFieldMetadata } from './partial-field-metadata.interface';
|
||||
import { ComputedPartialWorkspaceEntity } from './partial-object-metadata.interface';
|
||||
@ -33,9 +33,15 @@ export interface ComparatorDeleteResult<T> {
|
||||
|
||||
export type ObjectComparatorResult =
|
||||
| ComparatorSkipResult
|
||||
| ComparatorCreateResult<Omit<ComputedPartialWorkspaceEntity, 'fields'>>
|
||||
| ComparatorCreateResult<
|
||||
Omit<ComputedPartialWorkspaceEntity, 'fields' | 'indexMetadatas'>
|
||||
>
|
||||
| ComparatorUpdateResult<
|
||||
Partial<Omit<ComputedPartialWorkspaceEntity, 'fields'>> & { id: string }
|
||||
Partial<
|
||||
Omit<ComputedPartialWorkspaceEntity, 'fields' | 'indexMetadatas'>
|
||||
> & {
|
||||
id: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export type FieldComparatorResult =
|
||||
|
||||
@ -13,17 +13,22 @@ import { v4 as uuidV4 } from 'uuid';
|
||||
import { PartialFieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface';
|
||||
import { PartialIndexMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-index-metadata.interface';
|
||||
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import { FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
|
||||
import {
|
||||
FieldMetadataEntity,
|
||||
FieldMetadataType,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
|
||||
import { FieldMetadataUpdate } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory';
|
||||
import { ObjectMetadataUpdate } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-object.factory';
|
||||
import { WorkspaceSyncStorage } from 'src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage';
|
||||
import { capitalize } from 'src/utils/capitalize';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceMetadataUpdaterService {
|
||||
@ -241,10 +246,42 @@ export class WorkspaceMetadataUpdaterService {
|
||||
const convertIndexFieldMetadataForSaving = (
|
||||
column: string,
|
||||
order: number,
|
||||
) => {
|
||||
): DeepPartial<IndexFieldMetadataEntity> => {
|
||||
// Ensure correct type
|
||||
const fieldMetadata = originalObjectMetadataCollection
|
||||
.find((object) => object.id === indexMetadata.objectMetadataId)
|
||||
?.fields.find((field) => column === field.name);
|
||||
?.fields.find((field) => {
|
||||
if (field.name === column) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!isCompositeFieldMetadataType(field.type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const compositeType = compositeTypeDefinitions.get(
|
||||
field.type as CompositeFieldMetadataType,
|
||||
);
|
||||
|
||||
if (!compositeType) {
|
||||
throw new Error(
|
||||
`Composite type definition not found for type: ${field.type}`,
|
||||
);
|
||||
}
|
||||
|
||||
const columnNames = compositeType.properties.reduce(
|
||||
(acc, column) => {
|
||||
acc.push(`${field.name}${capitalize(column.name)}`);
|
||||
|
||||
return acc;
|
||||
},
|
||||
[] as string[],
|
||||
);
|
||||
|
||||
if (columnNames.includes(column)) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!fieldMetadata) {
|
||||
throw new Error(`
|
||||
|
||||
@ -51,7 +51,7 @@ export class WorkspaceSyncIndexMetadataService {
|
||||
// We're only interested in standard fields
|
||||
fields: { isCustom: false },
|
||||
},
|
||||
relations: ['dataSource', 'fields', 'indexes'],
|
||||
relations: ['dataSource', 'fields', 'indexMetadatas'],
|
||||
});
|
||||
|
||||
// Create map of object metadata & field metadata by unique identifier
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
import { ComputedPartialWorkspaceEntity } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-object-metadata.interface';
|
||||
import { ComputedPartialFieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface';
|
||||
import { ComputedPartialWorkspaceEntity } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-object-metadata.interface';
|
||||
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
|
||||
export class WorkspaceSyncStorage {
|
||||
// Object metadata
|
||||
private readonly _objectMetadataCreateCollection: Omit<
|
||||
ComputedPartialWorkspaceEntity,
|
||||
'fields'
|
||||
'fields' | 'indexMetadatas'
|
||||
>[] = [];
|
||||
private readonly _objectMetadataUpdateCollection: (Partial<
|
||||
Omit<ComputedPartialWorkspaceEntity, 'fields'>
|
||||
Omit<ComputedPartialWorkspaceEntity, 'fields' | 'indexMetadatas'>
|
||||
> & {
|
||||
id: string;
|
||||
})[] = [];
|
||||
@ -89,7 +89,7 @@ export class WorkspaceSyncStorage {
|
||||
}
|
||||
|
||||
addCreateObjectMetadata(
|
||||
object: Omit<ComputedPartialWorkspaceEntity, 'fields'>,
|
||||
object: Omit<ComputedPartialWorkspaceEntity, 'fields' | 'indexMetadatas'>,
|
||||
) {
|
||||
this._objectMetadataCreateCollection.push(object);
|
||||
}
|
||||
|
||||
@ -16,8 +16,8 @@ import {
|
||||
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.decorator';
|
||||
import { WorkspaceIsDeprecated } from 'src/engine/twenty-orm/decorators/workspace-is-deprecated.decorator';
|
||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
@ -66,6 +66,10 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
'The company website URL. We use this url to fetch the company icon',
|
||||
icon: 'IconLink',
|
||||
})
|
||||
/*
|
||||
TODO: add soon once we've confirmed it's stabled
|
||||
@WorkspaceIsUnique()
|
||||
*/
|
||||
[DOMAIN_NAME_FIELD_NAME]?: LinksMetadata;
|
||||
|
||||
@WorkspaceField({
|
||||
@ -295,6 +299,6 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
@WorkspaceIsSystem()
|
||||
@WorkspaceIndex({ indexType: IndexType.GIN })
|
||||
@WorkspaceFieldIndex({ indexType: IndexType.GIN })
|
||||
[SEARCH_VECTOR_FIELD.name]: any;
|
||||
}
|
||||
|
||||
@ -14,8 +14,8 @@ import {
|
||||
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.decorator';
|
||||
import { WorkspaceIsDeprecated } from 'src/engine/twenty-orm/decorators/workspace-is-deprecated.decorator';
|
||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||
@ -96,7 +96,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
],
|
||||
defaultValue: "'NEW'",
|
||||
})
|
||||
@WorkspaceIndex()
|
||||
@WorkspaceFieldIndex()
|
||||
stage: string;
|
||||
|
||||
@WorkspaceField({
|
||||
@ -251,6 +251,6 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
@WorkspaceIsSystem()
|
||||
@WorkspaceIndex({ indexType: IndexType.GIN })
|
||||
@WorkspaceFieldIndex({ indexType: IndexType.GIN })
|
||||
[SEARCH_VECTOR_FIELD.name]: any;
|
||||
}
|
||||
|
||||
@ -17,11 +17,12 @@ import {
|
||||
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.decorator';
|
||||
import { WorkspaceIsDeprecated } from 'src/engine/twenty-orm/decorators/workspace-is-deprecated.decorator';
|
||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceIsUnique } from 'src/engine/twenty-orm/decorators/workspace-is-unique.decorator';
|
||||
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||
import { PERSON_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
@ -70,6 +71,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: 'Contact’s Emails',
|
||||
icon: 'IconMail',
|
||||
})
|
||||
@WorkspaceIsUnique()
|
||||
[EMAILS_FIELD_NAME]: EmailsMetadata;
|
||||
|
||||
@WorkspaceField({
|
||||
@ -304,6 +306,6 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
@WorkspaceIsSystem()
|
||||
@WorkspaceIndex({ indexType: IndexType.GIN })
|
||||
@WorkspaceFieldIndex({ indexType: IndexType.GIN })
|
||||
[SEARCH_VECTOR_FIELD.name]: any;
|
||||
}
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||
import { VIEW_FIELD_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.viewField,
|
||||
@ -22,6 +22,12 @@ import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-
|
||||
})
|
||||
@WorkspaceIsNotAuditLogged()
|
||||
@WorkspaceIsSystem()
|
||||
/*
|
||||
TODO: add soon once we've confirmed it's stabled
|
||||
@WorkspaceIndex(['fieldMetadataId', 'viewId'], {
|
||||
isUnique: true,
|
||||
indexWhereClause: '"deletedAt" IS NULL',
|
||||
})*/
|
||||
export class ViewFieldWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_FIELD_STANDARD_FIELD_IDS.fieldMetadataId,
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||
import { VIEW_SORT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.viewSort,
|
||||
@ -24,6 +24,12 @@ import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-
|
||||
})
|
||||
@WorkspaceIsNotAuditLogged()
|
||||
@WorkspaceIsSystem()
|
||||
/*
|
||||
TODO: add soon once we've confirmed it's stabled
|
||||
@WorkspaceIndex(['fieldMetadataId', 'viewId'], {
|
||||
isUnique: true,
|
||||
indexWhereClause: '"deletedAt" IS NULL',
|
||||
})*/
|
||||
export class ViewSortWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_SORT_STANDARD_FIELD_IDS.fieldMetadataId,
|
||||
|
||||
@ -9,6 +9,7 @@ export interface ReflectMetadataTypeMap {
|
||||
['workspace:is-audit-logged-metadata-args']: false;
|
||||
['workspace:is-primary-field-metadata-args']: true;
|
||||
['workspace:is-deprecated-field-metadata-args']: true;
|
||||
['workspace:is-unique-metadata-args']: true;
|
||||
}
|
||||
|
||||
export class TypedReflect {
|
||||
|
||||
Reference in New Issue
Block a user