Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 57112cdecb | |||
| 1d84e23a1b | |||
| 2e893eed0d | |||
| b782ed4ca2 |
+87
@@ -0,0 +1,87 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Not, Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-18:delete-orphan-favorites',
|
||||
description:
|
||||
'Delete favorites whose viewId does not exist in workspace view metadata (fixes upgrade failure)',
|
||||
})
|
||||
export class DeleteOrphanFavoritesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
protected readonly logger = new Logger(DeleteOrphanFavoritesCommand.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(`Deleting orphan favorites for workspace ${workspaceId}`);
|
||||
|
||||
const { flatViewMaps } = await this.workspaceCacheService.getOrRecompute(
|
||||
workspaceId,
|
||||
['flatViewMaps'],
|
||||
);
|
||||
|
||||
const favoriteRepository =
|
||||
await this.twentyORMGlobalManager.getRepository<FavoriteWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'favorite',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const favoritesWithViewId = await favoriteRepository.find({
|
||||
where: {
|
||||
deletedAt: IsNull(),
|
||||
viewId: Not(IsNull()),
|
||||
},
|
||||
select: { id: true, viewId: true },
|
||||
});
|
||||
|
||||
const orphanFavoriteIds = favoritesWithViewId
|
||||
.filter(
|
||||
(favorite) =>
|
||||
!isDefined(flatViewMaps.universalIdentifierById[favorite.viewId]),
|
||||
)
|
||||
.map((favorite) => favorite.id);
|
||||
|
||||
if (orphanFavoriteIds.length === 0) {
|
||||
this.logger.log(`No orphan favorites found for workspace ${workspaceId}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would delete ${orphanFavoriteIds.length} orphan favorite(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await favoriteRepository.delete(orphanFavoriteIds);
|
||||
|
||||
this.logger.log(
|
||||
`Deleted ${orphanFavoriteIds.length} orphan favorite(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -284,7 +284,7 @@ export class MigrateFavoritesToNavigationMenuItemsCommand extends ActiveOrSuspen
|
||||
folderId: null,
|
||||
folderUniversalIdentifier: null,
|
||||
name: favoriteFolder.name,
|
||||
position: favoriteFolder.position,
|
||||
position: Math.round(favoriteFolder.position),
|
||||
workspaceId,
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
applicationUniversalIdentifier:
|
||||
@@ -316,7 +316,7 @@ export class MigrateFavoritesToNavigationMenuItemsCommand extends ActiveOrSuspen
|
||||
folderId: null,
|
||||
folderUniversalIdentifier: null,
|
||||
name: favoriteFolder.name,
|
||||
position: favoriteFolder.position,
|
||||
position: Math.round(favoriteFolder.position),
|
||||
workspaceId,
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
applicationUniversalIdentifier:
|
||||
@@ -521,7 +521,7 @@ export class MigrateFavoritesToNavigationMenuItemsCommand extends ActiveOrSuspen
|
||||
folderId,
|
||||
folderUniversalIdentifier: folderId,
|
||||
name: null,
|
||||
position: favorite.position,
|
||||
position: Math.round(favorite.position),
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
@@ -566,7 +566,7 @@ export class MigrateFavoritesToNavigationMenuItemsCommand extends ActiveOrSuspen
|
||||
folderId,
|
||||
folderUniversalIdentifier: folderId,
|
||||
name: null,
|
||||
position: favorite.position,
|
||||
position: Math.round(favorite.position),
|
||||
workspaceId,
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
applicationUniversalIdentifier:
|
||||
|
||||
+79
-12
@@ -2,8 +2,13 @@ import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import crypto from 'crypto';
|
||||
import { mkdtemp, mkdir, writeFile, readFile, rm } from 'fs/promises';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
import { build } from 'esbuild';
|
||||
import { Command } from 'nest-commander';
|
||||
import { NODE_ESM_CJS_BANNER } from 'twenty-shared/application';
|
||||
import { FileFolder, type Sources } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In, Repository } from 'typeorm';
|
||||
@@ -188,12 +193,18 @@ export class MigrateWorkflowCodeStepsCommand extends ActiveOrSuspendedWorkspaces
|
||||
oldLogicFunction.applicationId,
|
||||
);
|
||||
|
||||
const { builtContent, sourceContent } = await this.readOldFunctionFiles(
|
||||
const oldFiles = await this.readOldFunctionFiles(
|
||||
workspaceId,
|
||||
serverlessFunctionId,
|
||||
version,
|
||||
);
|
||||
|
||||
if (!isDefined(oldFiles)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { builtContent, sourceContent } = oldFiles;
|
||||
|
||||
const checksum = crypto
|
||||
.createHash('md5')
|
||||
.update(builtContent)
|
||||
@@ -236,33 +247,89 @@ export class MigrateWorkflowCodeStepsCommand extends ActiveOrSuspendedWorkspaces
|
||||
workspaceId: string,
|
||||
serverlessFunctionId: string,
|
||||
version: string,
|
||||
): Promise<{ builtContent: string; sourceContent: string }> {
|
||||
): Promise<{ builtContent: string; sourceContent: string } | null> {
|
||||
const workspacePrefix = `workspace-${workspaceId}`;
|
||||
|
||||
const builtSources = await this.fileStorageService.readFolderLegacy(
|
||||
`${workspacePrefix}/${OLD_BUILT_FOLDER}/${serverlessFunctionId}/${version}`,
|
||||
);
|
||||
let builtSources: Sources | null = null;
|
||||
|
||||
const sourceSources = await this.fileStorageService.readFolderLegacy(
|
||||
`${workspacePrefix}/${OLD_SOURCE_FOLDER}/${serverlessFunctionId}/${version}`,
|
||||
);
|
||||
try {
|
||||
builtSources = await this.fileStorageService.readFolderLegacy(
|
||||
`${workspacePrefix}/${OLD_BUILT_FOLDER}/${serverlessFunctionId}/${version}`,
|
||||
);
|
||||
} catch {
|
||||
this.logger.warn(
|
||||
`Built folder not found for serverless function ${serverlessFunctionId}/${version} in workspace ${workspaceId}, will build from source`,
|
||||
);
|
||||
}
|
||||
|
||||
let sourceSources: Sources;
|
||||
|
||||
try {
|
||||
sourceSources = await this.fileStorageService.readFolderLegacy(
|
||||
`${workspacePrefix}/${OLD_SOURCE_FOLDER}/${serverlessFunctionId}/${version}`,
|
||||
);
|
||||
} catch {
|
||||
this.logger.warn(
|
||||
`Source folder not found for serverless function ${serverlessFunctionId}/${version} in workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Old source layout may nest files under a `src/` key
|
||||
const sourceRoot =
|
||||
(sourceSources.src as Sources) ?? (sourceSources as Sources);
|
||||
|
||||
const builtContent = builtSources['index.mjs'] as string;
|
||||
const sourceContent = sourceRoot['index.ts'] as string;
|
||||
|
||||
if (!isDefined(builtContent) || !isDefined(sourceContent)) {
|
||||
throw new Error(
|
||||
`Missing index.mjs or index.ts for serverless function ${serverlessFunctionId}/${version} in workspace ${workspaceId}`,
|
||||
if (!isDefined(sourceContent)) {
|
||||
this.logger.warn(
|
||||
`Missing index.ts for serverless function ${serverlessFunctionId}/${version} in workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
let builtContent = builtSources?.['index.mjs'] as string | undefined;
|
||||
|
||||
if (!isDefined(builtContent)) {
|
||||
this.logger.log(
|
||||
`Building serverless function ${serverlessFunctionId}/${version} from source in workspace ${workspaceId}`,
|
||||
);
|
||||
builtContent = await this.buildSourceToMjs(sourceContent);
|
||||
}
|
||||
|
||||
return { builtContent, sourceContent };
|
||||
}
|
||||
|
||||
private async buildSourceToMjs(sourceContent: string): Promise<string> {
|
||||
const tempDir = await mkdtemp(join(tmpdir(), 'twenty-migrate-'));
|
||||
|
||||
try {
|
||||
const srcDir = join(tempDir, 'src');
|
||||
const outFile = join(tempDir, 'index.mjs');
|
||||
|
||||
await mkdir(srcDir, { recursive: true });
|
||||
await writeFile(join(srcDir, 'index.ts'), sourceContent);
|
||||
|
||||
await build({
|
||||
entryPoints: [join(srcDir, 'index.ts')],
|
||||
outfile: outFile,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
target: 'es2017',
|
||||
bundle: true,
|
||||
sourcemap: true,
|
||||
packages: 'external',
|
||||
banner: NODE_ESM_CJS_BANNER,
|
||||
});
|
||||
|
||||
return await readFile(outFile, 'utf-8');
|
||||
} finally {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
private async uploadFunctionFiles(
|
||||
workspaceId: string,
|
||||
applicationUniversalIdentifier: string,
|
||||
|
||||
+3
@@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { BackfillApplicationPackageFilesCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-backfill-application-package-files.command';
|
||||
import { DeleteFileRecordsAndUpdateTableCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-delete-all-files-and-update-table.command';
|
||||
import { DeleteOrphanFavoritesCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-delete-orphan-favorites.command';
|
||||
import { FixMorphRelationFieldNamesCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-fix-morph-relation-field-names.command';
|
||||
import { IdentifyWebhookMetadataCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-identify-webhook-metadata.command';
|
||||
import { MakeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-make-webhook-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
@@ -68,6 +69,7 @@ import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/tas
|
||||
GlobalWorkspaceDataSourceModule,
|
||||
],
|
||||
providers: [
|
||||
DeleteOrphanFavoritesCommand,
|
||||
FixMorphRelationFieldNamesCommand,
|
||||
MigrateAttachmentToMorphRelationsCommand,
|
||||
MigrateFavoritesToNavigationMenuItemsCommand,
|
||||
@@ -82,6 +84,7 @@ import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/tas
|
||||
BackfillApplicationPackageFilesCommand,
|
||||
],
|
||||
exports: [
|
||||
DeleteOrphanFavoritesCommand,
|
||||
FixMorphRelationFieldNamesCommand,
|
||||
MigrateAttachmentToMorphRelationsCommand,
|
||||
MigrateFavoritesToNavigationMenuItemsCommand,
|
||||
|
||||
+3
@@ -15,6 +15,7 @@ import { FixMorphRelationFieldNamesCommand } from 'src/database/commands/upgrade
|
||||
import { IdentifyWebhookMetadataCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-identify-webhook-metadata.command';
|
||||
import { MakeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-make-webhook-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MigrateAttachmentToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-attachment-to-morph-relations.command';
|
||||
import { DeleteOrphanFavoritesCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-delete-orphan-favorites.command';
|
||||
import { MigrateFavoritesToNavigationMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-favorites-to-navigation-menu-items.command';
|
||||
import { MigrateNoteTargetToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-note-target-to-morph-relations.command';
|
||||
import { MigrateTaskTargetToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-task-target-to-morph-relations.command';
|
||||
@@ -42,6 +43,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
protected readonly backfillApplicationPackageFilesCommand: BackfillApplicationPackageFilesCommand,
|
||||
protected readonly deleteFileRecordsAndUpdateTableCommand: DeleteFileRecordsAndUpdateTableCommand,
|
||||
protected readonly migrateAttachmentToMorphRelationsCommand: MigrateAttachmentToMorphRelationsCommand,
|
||||
protected readonly deleteOrphanFavoritesCommand: DeleteOrphanFavoritesCommand,
|
||||
protected readonly migrateFavoritesToNavigationMenuItemsCommand: MigrateFavoritesToNavigationMenuItemsCommand,
|
||||
protected readonly migrateNoteTargetToMorphRelationsCommand: MigrateNoteTargetToMorphRelationsCommand,
|
||||
protected readonly migrateTaskTargetToMorphRelationsCommand: MigrateTaskTargetToMorphRelationsCommand,
|
||||
@@ -62,6 +64,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
|
||||
const commands_1170: VersionCommands = [
|
||||
this.migrateAttachmentToMorphRelationsCommand,
|
||||
this.deleteOrphanFavoritesCommand,
|
||||
this.migrateFavoritesToNavigationMenuItemsCommand,
|
||||
this.migrateNoteTargetToMorphRelationsCommand,
|
||||
this.migrateTaskTargetToMorphRelationsCommand,
|
||||
|
||||
+6
-10
@@ -166,13 +166,12 @@ export class FlatNavigationMenuItemValidatorService {
|
||||
|
||||
if (
|
||||
isDefined(flatNavigationMenuItem.position) &&
|
||||
(!Number.isInteger(flatNavigationMenuItem.position) ||
|
||||
flatNavigationMenuItem.position < 0)
|
||||
!Number.isFinite(flatNavigationMenuItem.position)
|
||||
) {
|
||||
validationResult.errors.push({
|
||||
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
||||
message: t`Position must be a non-negative integer`,
|
||||
userFriendlyMessage: msg`Position must be a non-negative integer`,
|
||||
message: t`Position must be a finite number`,
|
||||
userFriendlyMessage: msg`Position must be a finite number`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -293,14 +292,11 @@ export class FlatNavigationMenuItemValidatorService {
|
||||
|
||||
const positionUpdate = flatEntityUpdate.position;
|
||||
|
||||
if (
|
||||
isDefined(positionUpdate) &&
|
||||
(!Number.isInteger(positionUpdate) || positionUpdate < 0)
|
||||
) {
|
||||
if (isDefined(positionUpdate) && !Number.isFinite(positionUpdate)) {
|
||||
validationResult.errors.push({
|
||||
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
||||
message: t`Position must be a non-negative integer`,
|
||||
userFriendlyMessage: msg`Position must be a non-negative integer`,
|
||||
message: t`Position must be a finite number`,
|
||||
userFriendlyMessage: msg`Position must be a finite number`,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user