Upgrade to Node22 (#12488)

BlocknoteJS requires an ESM module where our server is CJS, this forced
us to pin the server-util version, which led us to force the resolution
of several packages, leading to bugs downstream.

From Node 22.12 Node supports requiring ESM modules (available from Node
22.0 with a flag). So I upgrade the module.
I picked Node 22 and not Node 23 or Node 24 because 22 is the LTS and we
don't plan to change node versions frequently.

If you remain on Node 18, things should still mostly work, except if you
edit a Rich Text field.

I also starting changing the default runtime for Serverless Functions
which isn't directly related. This means new serverless functions will
be created on Node 22, but we will still need another PR to migrate
existing serverless functions before September (end of support by AWS).

(In this PR I also remove the upgrade commands from 0.43 since they rely
on Blocknote and I didn't want to have to deal with this)

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Félix Malfait
2025-06-06 18:35:30 +02:00
committed by GitHub
parent 0188b66280
commit 322c8a1852
39 changed files with 1882 additions and 3146 deletions

View File

@ -16,6 +16,7 @@
},
"dependencies": {
"@ai-sdk/openai": "^1.3.22",
"@blocknote/server-util": "^0.31.1",
"@clickhouse/client": "^1.11.0",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@graphql-yoga/nestjs": "patch:@graphql-yoga/nestjs@2.1.0#./patches/@graphql-yoga+nestjs+2.1.0.patch",
@ -89,7 +90,7 @@
"typescript": "5.3.3"
},
"engines": {
"node": "^18.17.1",
"node": "^22.12.0",
"npm": "please-use-yarn",
"yarn": "^4.0.2"
}

View File

@ -18,7 +18,7 @@
"options": {
"cwd": "packages/twenty-server",
"commands": [
"NODE_ENV=test nx jest --config ./jest-integration.config.ts"
"NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" nx jest --config ./jest-integration.config.ts"
]
},
"parallel": false,
@ -26,7 +26,7 @@
"with-db-reset": {
"cwd": "packages/twenty-server",
"commands": [
"NODE_ENV=test nx database:reset > reset-logs.log && NODE_ENV=test nx jest --config ./jest-integration.config.ts"
"NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" nx database:reset > reset-logs.log && NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" nx jest --config ./jest-integration.config.ts"
]
}
}

View File

@ -1,208 +0,0 @@
import { InjectRepository } from '@nestjs/typeorm';
import chalk from 'chalk';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { FieldMetadataDefaultOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { tasksAssignedToMeView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-assigned-to-me';
import { TASK_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 { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity';
import { ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity';
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
@Command({
name: 'upgrade:0-43:add-tasks-assigned-to-me-view',
description: 'Add tasks assigned to me view',
})
export class AddTasksAssignedToMeViewCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
override async runOnWorkspace({
index,
total,
workspaceId,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
);
await this.createTasksAssignedToMeView(workspaceId);
this.logger.log(
chalk.green(`Command completed for workspace ${workspaceId}.`),
);
}
private async createTasksAssignedToMeView(
workspaceId: string,
): Promise<void> {
const objectMetadata = await this.objectMetadataRepository.find({
where: { workspaceId },
relations: ['fields'],
});
const objectMetadataMap = objectMetadata.reduce((acc, object) => {
// @ts-expect-error legacy noImplicitAny
acc[object.standardId ?? ''] = {
id: object.id,
fields: object.fields.reduce((acc, field) => {
// @ts-expect-error legacy noImplicitAny
acc[field.standardId ?? ''] = field.id;
return acc;
}, {}),
};
return acc;
}, {});
const taskObjectMetadata = objectMetadata.find(
(object) => object.standardId === STANDARD_OBJECT_IDS.task,
);
if (!taskObjectMetadata) {
throw new Error(`Task object not found for workspace ${workspaceId}`);
}
const viewRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewWorkspaceEntity>(
workspaceId,
'view',
);
const existingView = await viewRepository.findOne({
where: {
name: 'Assigned to Me',
objectMetadataId: taskObjectMetadata.id,
},
});
if (existingView) {
this.logger.log(
chalk.yellow(
`"Assigned to Me" view already exists for workspace ${workspaceId}`,
),
);
return;
}
const viewDefinition = tasksAssignedToMeView(objectMetadataMap);
const viewId = v4();
const insertedView = await viewRepository.save({
id: viewId,
name: viewDefinition.name,
objectMetadataId: viewDefinition.objectMetadataId,
type: viewDefinition.type,
position: viewDefinition.position,
icon: viewDefinition.icon,
kanbanFieldMetadataId: viewDefinition.kanbanFieldMetadataId,
});
if (viewDefinition.fields && viewDefinition.fields.length > 0) {
const viewFieldRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewFieldWorkspaceEntity>(
workspaceId,
'viewField',
);
const viewFields = viewDefinition.fields.map((field) => ({
fieldMetadataId: field.fieldMetadataId,
position: field.position,
isVisible: field.isVisible,
size: field.size,
viewId: insertedView.id,
}));
await viewFieldRepository.save(viewFields);
}
if (viewDefinition.filters && viewDefinition.filters.length > 0) {
const viewFilterRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewFilterWorkspaceEntity>(
workspaceId,
'viewFilter',
);
const viewFilters = viewDefinition.filters.map((filter) => ({
fieldMetadataId: filter.fieldMetadataId,
displayValue: filter.displayValue,
operand: filter.operand,
value: filter.value,
viewId: insertedView.id,
}));
await viewFilterRepository.save(viewFilters);
}
await this.createTasksAssignedToMeViewGroups(workspaceId, insertedView.id);
}
private async createTasksAssignedToMeViewGroups(
workspaceId: string,
viewId: string,
) {
const taskStatusFieldMetadata = await this.fieldMetadataRepository.findOne({
where: {
workspaceId,
standardId: TASK_STANDARD_FIELD_IDS.status,
},
});
if (!taskStatusFieldMetadata) {
throw new Error(
`Task status field metadata not found for workspace ${workspaceId}`,
);
}
const optionValueViewGroups = taskStatusFieldMetadata.options.map(
(taskStatusOption: FieldMetadataDefaultOption, index) =>
({
fieldMetadataId: taskStatusFieldMetadata.id,
viewId,
fieldValue: taskStatusOption.value,
position: index,
}) satisfies Partial<ViewGroupWorkspaceEntity>,
);
const noValueViewGroup: Partial<ViewGroupWorkspaceEntity> = {
fieldMetadataId: taskStatusFieldMetadata.id,
viewId,
fieldValue: '',
position: optionValueViewGroups.length,
};
const viewGroups = [...optionValueViewGroups, noValueViewGroup];
const viewGroupRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewGroupWorkspaceEntity>(
workspaceId,
'viewGroup',
);
await viewGroupRepository.insert(viewGroups);
}
}

View File

@ -1,51 +0,0 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { 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-43:migrate-is-searchable-for-custom-object-metadata',
description: 'Set isSearchable true for custom object metadata',
})
export class MigrateIsSearchableForCustomObjectMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectRepository(ObjectMetadataEntity, 'metadata')
protected readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
override async runOnWorkspace({
index,
total,
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
);
if (!options.dryRun) {
await this.objectMetadataRepository.update(
{
workspaceId,
isCustom: true,
},
{
isSearchable: true,
},
);
}
}
}

View File

@ -1,275 +0,0 @@
import { InjectRepository } from '@nestjs/typeorm';
import { ServerBlockNoteEditor } from '@blocknote/server-util';
import chalk from 'chalk';
import { Command } from 'nest-commander';
import { FieldMetadataType } from 'twenty-shared/types';
import { Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandOptions,
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
type MigrateRichTextContentArgs = {
richTextFieldsWithObjectMetadata: RichTextFieldsWithObjectMetadata[];
workspaceId: string;
options: ActiveOrSuspendedWorkspacesMigrationCommandOptions;
};
type RichTextFieldsWithObjectMetadata = {
richTextField: FieldMetadataEntity;
objectMetadata: ObjectMetadataEntity | null;
};
type ProcessRichTextFieldsArgs = {
richTextFields: FieldMetadataEntity[];
workspaceId: string;
};
@Command({
name: 'upgrade:0-43:migrate-rich-text-content-patch',
description: 'Migrate RICH_TEXT content from v1 to v2',
})
export class MigrateRichTextContentPatchCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(FeatureFlag, 'core')
protected readonly featureFlagRepository: Repository<FeatureFlag>,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
override async runOnWorkspace({
index,
total,
options,
workspaceId,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
`Running MigrateRichTextContentPatchCommand for workspace ${workspaceId} ${index + 1}/${total}`,
);
if (await this.hasRichTextV2FeatureFlag(workspaceId)) {
this.logger.log(
chalk.yellow(
'Rich text v2 feature flag is enabled, skipping migration',
),
);
return;
}
const richTextFields = await this.fieldMetadataRepository.find({
where: {
workspaceId,
type: FieldMetadataType.RICH_TEXT,
},
});
if (!richTextFields.length) {
this.logger.log(
chalk.yellow('No RICH_TEXT fields found in this workspace'),
);
return;
}
this.logger.log(`Found ${richTextFields.length} RICH_TEXT fields`);
const richTextFieldsWithObjectMetadata =
await this.getRichTextFieldsWithObjectMetadata({
richTextFields,
workspaceId,
});
await this.migrateToNewRichTextFieldsColumn({
richTextFieldsWithObjectMetadata,
workspaceId,
options,
});
this.logger.log(
chalk.green(`Command completed for workspace ${workspaceId}`),
);
}
private async hasRichTextV2FeatureFlag(
workspaceId: string,
): Promise<boolean> {
return await this.featureFlagRepository.exists({
where: {
workspaceId,
key: 'IS_RICH_TEXT_V2_ENABLED' as FeatureFlagKey,
value: true,
},
});
}
private async getRichTextFieldsWithObjectMetadata({
richTextFields,
workspaceId,
}: ProcessRichTextFieldsArgs): Promise<RichTextFieldsWithObjectMetadata[]> {
const richTextFieldsWithObjectMetadata: RichTextFieldsWithObjectMetadata[] =
[];
for (const richTextField of richTextFields) {
const objectMetadata = await this.objectMetadataRepository.findOne({
where: { id: richTextField.objectMetadataId },
relations: {
fields: true,
},
});
if (objectMetadata === null) {
this.logger.log(
`Object metadata not found for rich text field ${richTextField.name} in workspace ${workspaceId}`,
);
}
richTextFieldsWithObjectMetadata.push({
richTextField,
objectMetadata,
});
}
return richTextFieldsWithObjectMetadata;
}
private jsonParseOrSilentlyFail(input: string): null | unknown {
try {
return JSON.parse(input);
} catch (e) {
return null;
}
}
private async getMarkdownFieldValue({
blocknoteFieldValue,
serverBlockNoteEditor,
}: {
blocknoteFieldValue: string | null;
serverBlockNoteEditor: ServerBlockNoteEditor;
}): Promise<string | null> {
const blocknoteFieldValueIsDefined =
blocknoteFieldValue !== null &&
blocknoteFieldValue !== undefined &&
blocknoteFieldValue !== '{}';
if (!blocknoteFieldValueIsDefined) {
return null;
}
const jsonParsedblocknoteFieldValue =
this.jsonParseOrSilentlyFail(blocknoteFieldValue);
if (jsonParsedblocknoteFieldValue === null) {
return null;
}
if (!Array.isArray(jsonParsedblocknoteFieldValue)) {
this.logger.log(
`blocknoteFieldValue is defined and is not an array got ${blocknoteFieldValue}`,
);
return null;
}
let markdown: string | null = null;
try {
markdown = await serverBlockNoteEditor.blocksToMarkdownLossy(
jsonParsedblocknoteFieldValue,
);
} catch (error) {
this.logger.log(
`Error converting blocknote to markdown for ${blocknoteFieldValue}`,
);
}
return markdown;
}
private async migrateToNewRichTextFieldsColumn({
richTextFieldsWithObjectMetadata,
workspaceId,
options,
}: MigrateRichTextContentArgs) {
const serverBlockNoteEditor = ServerBlockNoteEditor.create();
for (const {
richTextField,
objectMetadata,
} of richTextFieldsWithObjectMetadata) {
if (objectMetadata === null) {
this.logger.log(
`Object metadata not found for rich text field ${richTextField.name} in workspace ${workspaceId}`,
);
continue;
}
const schemaName =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const workspaceDataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId,
shouldFailIfMetadataNotFound: false,
});
const rows = await workspaceDataSource.query(
`SELECT id, "${richTextField.name}" FROM "${schemaName}"."${computeTableName(objectMetadata.nameSingular, objectMetadata.isCustom)}" WHERE "${richTextField.name}" IS NOT NULL`,
undefined, // parameters
undefined, // queryRunner
{
shouldBypassPermissionChecks: true,
},
);
this.logger.log(`Generating markdown for ${rows.length} records`);
for (const row of rows) {
const blocknoteFieldValue = row[richTextField.name];
const markdownFieldValue = await this.getMarkdownFieldValue({
blocknoteFieldValue,
serverBlockNoteEditor,
});
if (!options.dryRun) {
try {
await workspaceDataSource.query(
`UPDATE "${schemaName}"."${computeTableName(objectMetadata.nameSingular, objectMetadata.isCustom)}" SET "${richTextField.name}V2Blocknote" = $1, "${richTextField.name}V2Markdown" = $2 WHERE id = $3`,
[blocknoteFieldValue, markdownFieldValue, row.id],
undefined, // queryRunner
{
shouldBypassPermissionChecks: true,
},
);
} catch (error) {
this.logger.log(
chalk.red(
`Error updating rich text field ${richTextField.name} for record ${row.id} in workspace ${workspaceId}`,
),
);
}
}
}
}
}
}

View File

@ -1,96 +0,0 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { SearchVectorService } from 'src/engine/metadata-modules/search-vector/search-vector.service';
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
import { SEARCH_FIELDS_FOR_NOTES } from 'src/modules/note/standard-objects/note.workspace-entity';
import { SEARCH_FIELDS_FOR_TASKS } from 'src/modules/task/standard-objects/task.workspace-entity';
@Command({
name: 'upgrade:0-43:migrate-search-vector-on-note-and-task-entities',
description: 'Migrate search vector on note and task entities',
})
export class MigrateSearchVectorOnNoteAndTaskEntitiesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectRepository(FeatureFlag, 'core')
protected readonly featureFlagRepository: Repository<FeatureFlag>,
@InjectRepository(ObjectMetadataEntity, 'metadata')
protected readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly searchVectorService: SearchVectorService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
override async runOnWorkspace({
index,
total,
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
);
const noteObjectMetadata =
await this.objectMetadataRepository.findOneOrFail({
select: ['id'],
where: {
workspaceId,
nameSingular: 'note',
},
});
if (!options.dryRun) {
await this.searchVectorService.updateSearchVector(
noteObjectMetadata.id,
SEARCH_FIELDS_FOR_NOTES,
workspaceId,
);
}
const taskObjectMetadata =
await this.objectMetadataRepository.findOneOrFail({
select: ['id'],
where: {
workspaceId,
nameSingular: 'task',
},
});
if (!options.dryRun) {
await this.searchVectorService.updateSearchVector(
taskObjectMetadata.id,
SEARCH_FIELDS_FOR_TASKS,
workspaceId,
);
}
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
workspaceId,
);
await this.workspaceMetadataVersionService.incrementMetadataVersion(
workspaceId,
);
this.logger.log(
`Migrated search vector on note and task entities for workspace ${workspaceId}`,
);
}
}

View File

@ -1,99 +0,0 @@
import { InjectRepository } from '@nestjs/typeorm';
import chalk from 'chalk';
import { Command } from 'nest-commander';
import { In, Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { 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 { ViewOpenRecordInType } from 'src/modules/view/standard-objects/view.workspace-entity';
@Command({
name: 'upgrade:0-43:update-default-view-record-opening-on-workflow-objects',
description:
'Update default view record opening on workflow objects to record page',
})
export class UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(ObjectMetadataEntity, 'metadata')
protected readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
override async runOnWorkspace({
index,
total,
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
);
const workflowObjectsMetadata = await this.objectMetadataRepository.find({
select: ['id'],
where: {
workspaceId,
standardId: In([
STANDARD_OBJECT_IDS.workflow,
STANDARD_OBJECT_IDS.workflowVersion,
STANDARD_OBJECT_IDS.workflowRun,
]),
},
});
if (workflowObjectsMetadata.length === 0) {
this.logger.log(
chalk.yellow(`No workflow objects found for workspace ${workspaceId}`),
);
return;
}
if (!options.dryRun) {
await this.updateDefaultViewsRecordOpening(
workflowObjectsMetadata.map((metadata) => metadata.id),
workspaceId,
);
}
this.logger.log(
chalk.green(`Command completed for workspace ${workspaceId}.`),
);
}
private async updateDefaultViewsRecordOpening(
workflowObjectMetadataIds: string[],
workspaceId: string,
): Promise<void> {
const failOnMetadataCacheMiss = false;
const viewRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
'view',
{
shouldFailIfMetadataNotFound: failOnMetadataCacheMiss,
},
);
await viewRepository.update(
{
objectMetadataId: In(workflowObjectMetadataIds),
key: 'INDEX',
},
{
openRecordIn: ViewOpenRecordInType.RECORD_PAGE,
},
);
}
}

View File

@ -1,45 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AddTasksAssignedToMeViewCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-add-tasks-assigned-to-me-view.command';
import { MigrateIsSearchableForCustomObjectMetadataCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-is-searchable-for-custom-object-metadata.command';
import { MigrateRichTextContentPatchCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-rich-text-content-patch.command';
import { MigrateSearchVectorOnNoteAndTaskEntitiesCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-search-vector-on-note-and-task-entities.command';
import { UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-update-default-view-record-opening-on-workflow-objects.command';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { SearchVectorModule } from 'src/engine/metadata-modules/search-vector/search-vector.module';
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
@Module({
imports: [
TypeOrmModule.forFeature([Workspace, FeatureFlag], 'core'),
TypeOrmModule.forFeature(
[FieldMetadataEntity, ObjectMetadataEntity],
'metadata',
),
WorkspaceDataSourceModule,
SearchVectorModule,
WorkspaceMigrationRunnerModule,
WorkspaceMetadataVersionModule,
],
providers: [
MigrateRichTextContentPatchCommand,
MigrateSearchVectorOnNoteAndTaskEntitiesCommand,
UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand,
MigrateIsSearchableForCustomObjectMetadataCommand,
AddTasksAssignedToMeViewCommand,
],
exports: [
MigrateRichTextContentPatchCommand,
MigrateSearchVectorOnNoteAndTaskEntitiesCommand,
UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand,
MigrateIsSearchableForCustomObjectMetadataCommand,
AddTasksAssignedToMeViewCommand,
],
})
export class V0_43_UpgradeVersionCommandModule {}

View File

@ -1,7 +1,6 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { V0_43_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/0-43/0-43-upgrade-version-command.module';
import { V0_44_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/0-44/0-44-upgrade-version-command.module';
import { V0_50_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/0-50/0-50-upgrade-version-command.module';
import { V0_51_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/0-51/0-51-upgrade-version-command.module';
@ -19,7 +18,6 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
@Module({
imports: [
TypeOrmModule.forFeature([Workspace], 'core'),
V0_43_UpgradeVersionCommandModule,
V0_44_UpgradeVersionCommandModule,
V0_50_UpgradeVersionCommandModule,
V0_51_UpgradeVersionCommandModule,

View File

@ -15,11 +15,6 @@ import {
UpgradeCommandRunner,
VersionCommands,
} from 'src/database/commands/command-runners/upgrade.command-runner';
import { AddTasksAssignedToMeViewCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-add-tasks-assigned-to-me-view.command';
import { MigrateIsSearchableForCustomObjectMetadataCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-is-searchable-for-custom-object-metadata.command';
import { MigrateRichTextContentPatchCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-rich-text-content-patch.command';
import { MigrateSearchVectorOnNoteAndTaskEntitiesCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-search-vector-on-note-and-task-entities.command';
import { UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-update-default-view-record-opening-on-workflow-objects.command';
import { InitializePermissionsCommand } from 'src/database/commands/upgrade-version-command/0-44/0-44-initialize-permissions.command';
import { UpdateViewAggregateOperationsCommand } from 'src/database/commands/upgrade-version-command/0-44/0-44-update-view-aggregate-operations.command';
import { UpgradeCreatedByEnumCommand } from 'src/database/commands/upgrade-version-command/0-51/0-51-update-workflow-trigger-type-enum.command';
@ -152,13 +147,6 @@ export class UpgradeCommand extends UpgradeCommandRunner {
private readonly databaseMigrationService: DatabaseMigrationService,
// 0.43 Commands
protected readonly migrateRichTextContentPatchCommand: MigrateRichTextContentPatchCommand,
protected readonly addTasksAssignedToMeViewCommand: AddTasksAssignedToMeViewCommand,
protected readonly migrateIsSearchableForCustomObjectMetadataCommand: MigrateIsSearchableForCustomObjectMetadataCommand,
protected readonly updateDefaultViewRecordOpeningOnWorkflowObjectsCommand: UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand,
protected readonly migrateSearchVectorOnNoteAndTaskEntitiesCommand: MigrateSearchVectorOnNoteAndTaskEntitiesCommand,
// 0.44 Commands
protected readonly initializePermissionsCommand: InitializePermissionsCommand,
protected readonly updateViewAggregateOperationsCommand: UpdateViewAggregateOperationsCommand,
@ -191,18 +179,6 @@ export class UpgradeCommand extends UpgradeCommandRunner {
syncWorkspaceMetadataCommand,
);
const commands_043: VersionCommands = {
beforeSyncMetadata: [
this.migrateRichTextContentPatchCommand,
this.migrateIsSearchableForCustomObjectMetadataCommand,
this.migrateSearchVectorOnNoteAndTaskEntitiesCommand,
this.migrateIsSearchableForCustomObjectMetadataCommand,
],
afterSyncMetadata: [
this.updateDefaultViewRecordOpeningOnWorkflowObjectsCommand,
this.addTasksAssignedToMeViewCommand,
],
};
const commands_044: VersionCommands = {
beforeSyncMetadata: [
this.initializePermissionsCommand,
@ -251,7 +227,6 @@ export class UpgradeCommand extends UpgradeCommandRunner {
};
this.allCommands = {
'0.43.0': commands_043,
'0.44.0': commands_044,
'0.50.0': commands_050,
'0.51.0': commands_051,

View File

@ -40,6 +40,11 @@ export const seedFeatureFlags = async (
workspaceId: workspaceId,
value: false,
},
{
key: FeatureFlagKey.IS_AI_ENABLED,
workspaceId: workspaceId,
value: true,
},
])
.execute();
};

View File

@ -0,0 +1,21 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class UpdateServerlessFunctionDefaultRuntimeToNode221749205425841
implements MigrationInterface
{
name = 'UpdateServerlessFunctionDefaultRuntimeToNode221749205425841';
public async up(queryRunner: QueryRunner): Promise<void> {
// Update the default value for the runtime column to nodejs22.x
await queryRunner.query(
`ALTER TABLE "core"."serverlessFunction" ALTER COLUMN "runtime" SET DEFAULT 'nodejs22.x'`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Revert the default value back to nodejs18.x
await queryRunner.query(
`ALTER TABLE "core"."serverlessFunction" ALTER COLUMN "runtime" SET DEFAULT 'nodejs18.x'`,
);
}
}

View File

@ -1,6 +1,5 @@
import { Injectable } from '@nestjs/common';
import { ServerBlockNoteEditor } from '@blocknote/server-util';
import { isNonEmptyString } from '@sniptt/guards';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@ -101,6 +100,8 @@ export class RecordInputTransformerService {
): Promise<RichTextV2Metadata> {
const parsedValue = richTextV2ValueSchema.parse(richTextValue);
const { ServerBlockNoteEditor } = await import('@blocknote/server-util');
const serverBlockNoteEditor = ServerBlockNoteEditor.create();
// Patch: Handle cases where blocknote to markdown conversion fails for certain block types (custom/code blocks)

View File

@ -1,10 +1,9 @@
import * as fs from 'fs/promises';
import { join } from 'path';
import ts, { transpileModule } from 'typescript';
import {
CreateFunctionCommandInput,
CreateFunctionCommand,
CreateFunctionCommandInput,
DeleteFunctionCommand,
GetFunctionCommand,
InvokeCommand,
@ -13,14 +12,15 @@ import {
LambdaClientConfig,
ListLayerVersionsCommand,
ListLayerVersionsCommandInput,
LogType,
PublishLayerVersionCommand,
PublishLayerVersionCommandInput,
ResourceNotFoundException,
waitUntilFunctionUpdatedV2,
LogType,
} from '@aws-sdk/client-lambda';
import { AssumeRoleCommand, STSClient } from '@aws-sdk/client-sts';
import { isDefined } from 'twenty-shared/utils';
import ts, { transpileModule } from 'typescript';
import {
ServerlessDriver,
@ -28,8 +28,11 @@ import {
} from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { readFileContent } from 'src/engine/core-modules/file-storage/utils/read-file-content';
import { COMMON_LAYER_NAME } from 'src/engine/core-modules/serverless/drivers/constants/common-layer-name';
import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name';
import { copyAndBuildDependencies } from 'src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies';
import { copyExecutor } from 'src/engine/core-modules/serverless/drivers/utils/copy-executor';
import { createZipFile } from 'src/engine/core-modules/serverless/drivers/utils/create-zip-file';
import {
LambdaBuildDirectoryManager,
@ -45,9 +48,6 @@ import {
ServerlessFunctionException,
ServerlessFunctionExceptionCode,
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
import { copyExecutor } from 'src/engine/core-modules/serverless/drivers/utils/copy-executor';
import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name';
import { readFileContent } from 'src/engine/core-modules/file-storage/utils/read-file-content';
const UPDATE_FUNCTION_DURATION_TIMEOUT_IN_SECONDS = 60;
const CREDENTIALS_DURATION_IN_SECONDS = 60 * 60; // 1h
@ -172,7 +172,10 @@ export class LambdaDriver implements ServerlessDriver {
Content: {
ZipFile: await fs.readFile(lambdaZipPath),
},
CompatibleRuntimes: [ServerlessFunctionRuntime.NODE18],
CompatibleRuntimes: [
ServerlessFunctionRuntime.NODE18,
ServerlessFunctionRuntime.NODE22,
],
Description: `${version}`,
};

View File

@ -15,6 +15,7 @@ const DEFAULT_SERVERLESS_TIMEOUT_SECONDS = 300; // 5 minutes
export enum ServerlessFunctionRuntime {
NODE18 = 'nodejs18.x',
NODE22 = 'nodejs22.x',
}
@Entity('serverlessFunction')
@ -38,7 +39,7 @@ export class ServerlessFunctionEntity {
@Column({ nullable: true, type: 'jsonb' })
latestVersionInputSchema: InputSchema;
@Column({ nullable: false, default: ServerlessFunctionRuntime.NODE18 })
@Column({ nullable: false, default: ServerlessFunctionRuntime.NODE22 })
runtime: ServerlessFunctionRuntime;
@Column({ nullable: false, default: DEFAULT_SERVERLESS_TIMEOUT_SECONDS })