Workspace metadata migration v2 runner init file structure and services (#13242)

# Introduction
In this PR we initialize strictly typed services for both schema and
metadata migration runner.
Just scaffolding the file tree and services instances for incoming
parallel development with @Weiko

# Conclusion
Nothing is immuable here ! ( and might change in the future ) main goal
was to avoid upcoming conflicts and share same vision
As always any suggestion are more than welcomed !
This commit is contained in:
Paul Rastoin
2025-07-17 12:11:36 +02:00
committed by GitHub
parent 530a7dea86
commit 2fb7390965
21 changed files with 461 additions and 17 deletions

View File

@ -1,5 +1,6 @@
import { EachTestingContext } from 'twenty-shared/testing';
import { ConvertActionTypeToCamelCase } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/convert-action-type-to-camel-case.type';
import { WorkspaceMigrationActionTypeV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2';
import { WorkspaceMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.service';
@ -7,11 +8,6 @@ type WorkspaceBuilderArgs = Parameters<
typeof WorkspaceMigrationBuilderV2Service.prototype.build
>[0];
type ConvertActionTypeToCamelCase<T extends string> =
T extends `${infer Before}_${infer After}`
? `${Before}${Capitalize<After>}`
: T;
export type CamelCasedWorkspaceMigrationActionsType =
ConvertActionTypeToCamelCase<WorkspaceMigrationActionTypeV2>;

View File

@ -0,0 +1,4 @@
export type ConvertActionTypeToCamelCase<T extends string> =
T extends `${infer Before}_${infer After}`
? `${Before}${Capitalize<After>}`
: T;

View File

@ -31,3 +31,6 @@ export type WorkspaceMigrationFieldActionV2 =
| CreateFieldAction
| UpdateFieldAction
| DeleteFieldAction;
export type WorkspaceMigrationFieldActionTypeV2 =
WorkspaceMigrationFieldActionV2['type'];

View File

@ -13,3 +13,6 @@ export type DeleteIndexAction = {
export type WorkspaceMigrationIndexActionV2 =
| CreateIndexAction
| DeleteIndexAction;
export type WorkspaceMigrationIndexActionTypeV2 =
WorkspaceMigrationIndexActionV2['type'];

View File

@ -29,3 +29,6 @@ export type WorkspaceMigrationObjectActionV2 =
| CreateObjectAction
| UpdateObjectAction
| DeleteObjectAction;
export type WorkspaceMigrationObjectActionTypeV2 =
WorkspaceMigrationObjectActionV2['type'];

View File

@ -6,6 +6,6 @@ import { WorkspaceMigrationBuilderV2Service } from 'src/engine/workspace-manager
@Module({
imports: [FeatureFlagModule],
providers: [WorkspaceMigrationBuilderV2Service],
exports: [],
exports: [WorkspaceMigrationBuilderV2Service],
})
export class WorkspaceMigrationBuilderV2Module {}

View File

@ -0,0 +1,17 @@
import { ConvertActionTypeToCamelCase } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/convert-action-type-to-camel-case.type';
import {
WorkspaceMigrationActionTypeV2,
WorkspaceMigrationActionV2,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
export type RunnerMethodForActionType<
TAction extends WorkspaceMigrationActionTypeV2,
TRunner extends 'metadata' | 'schema',
> = {
[P in TAction as `run${Capitalize<ConvertActionTypeToCamelCase<P>>}${Capitalize<TRunner>}Migration`]: (
arg: WorkspaceMigrationActionRunnerArgs<
Extract<WorkspaceMigrationActionV2, { type: P }>
>,
) => Promise<void>;
};

View File

@ -0,0 +1,10 @@
import { QueryRunner } from 'typeorm';
import { WorkspaceMigrationActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2';
export type WorkspaceMigrationActionRunnerArgs<
T extends WorkspaceMigrationActionV2,
> = {
queryRunner: QueryRunner;
action: T;
};

View File

@ -0,0 +1,8 @@
import { QueryRunner } from 'typeorm';
import { WorkspaceMigrationV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-v2';
export type WorkspaceMigrationRunnerArgs = {
workspaceMigration: WorkspaceMigrationV2;
queryRunner: QueryRunner;
};

View File

@ -0,0 +1,32 @@
import { Injectable } from '@nestjs/common';
import {
CreateFieldAction,
DeleteFieldAction,
UpdateFieldAction,
WorkspaceMigrationFieldActionTypeV2,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2';
import { RunnerMethodForActionType } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/runner-method-for-action-type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
@Injectable()
export class WorkspaceMetadataFieldActionRunnerService
implements
RunnerMethodForActionType<WorkspaceMigrationFieldActionTypeV2, 'metadata'>
{
runDeleteFieldMetadataMigration = async (
_action: WorkspaceMigrationActionRunnerArgs<DeleteFieldAction>,
) => {
return;
};
runCreateFieldMetadataMigration = async (
_action: WorkspaceMigrationActionRunnerArgs<CreateFieldAction>,
) => {
return;
};
runUpdateFieldMetadataMigration = async (
_action: WorkspaceMigrationActionRunnerArgs<UpdateFieldAction>,
) => {
return;
};
}

View File

@ -0,0 +1,26 @@
import { Injectable } from '@nestjs/common';
import {
CreateIndexAction,
DeleteIndexAction,
WorkspaceMigrationIndexActionTypeV2,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-index-action-v2';
import { RunnerMethodForActionType } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/runner-method-for-action-type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
@Injectable()
export class WorkspaceMetadataIndexActionRunnerService
implements
RunnerMethodForActionType<WorkspaceMigrationIndexActionTypeV2, 'metadata'>
{
runDeleteIndexMetadataMigration = async (
_action: WorkspaceMigrationActionRunnerArgs<DeleteIndexAction>,
) => {
return;
};
runCreateIndexMetadataMigration = async (
_action: WorkspaceMigrationActionRunnerArgs<CreateIndexAction>,
) => {
return;
};
}

View File

@ -0,0 +1,81 @@
import { Injectable } from '@nestjs/common';
import { assertUnreachable } from 'twenty-shared/utils';
import { WorkspaceMigrationRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-runner-args.type';
import { WorkspaceMetadataFieldActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-metadata-migration-runner/workspace-metadata-field-action-runner.service';
import { WorkspaceMetadataIndexActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-metadata-migration-runner/workspace-metadata-index-action-runner.service';
import { WorkspaceMetadataObjectActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-metadata-migration-runner/workspace-metadata-object-action-runner.service';
@Injectable()
export class WorkspaceMetadataMigrationRunnerService {
constructor(
private readonly workspaceMetadataObjectMigrationRunnerService: WorkspaceMetadataObjectActionRunnerService,
private readonly workspaceMetadataIndexMigrationRunnerService: WorkspaceMetadataIndexActionRunnerService,
private readonly workspaceMetadataFieldMigrationRunnerService: WorkspaceMetadataFieldActionRunnerService,
) {}
runWorkspaceMetadataMigration = async ({
workspaceMigration,
queryRunner,
}: WorkspaceMigrationRunnerArgs) => {
for (const action of workspaceMigration.actions) {
switch (action.type) {
case 'delete_object': {
await this.workspaceMetadataObjectMigrationRunnerService.runDeleteObjectMetadataMigration(
{ action, queryRunner },
);
break;
}
case 'create_object': {
await this.workspaceMetadataObjectMigrationRunnerService.runCreateObjectMetadataMigration(
{ action, queryRunner },
);
break;
}
case 'update_object': {
await this.workspaceMetadataObjectMigrationRunnerService.runUpdateObjectMetadataMigration(
{ action, queryRunner },
);
break;
}
case 'create_field': {
await this.workspaceMetadataFieldMigrationRunnerService.runCreateFieldMetadataMigration(
{ action, queryRunner },
);
break;
}
case 'update_field': {
await this.workspaceMetadataFieldMigrationRunnerService.runUpdateFieldMetadataMigration(
{ action, queryRunner },
);
break;
}
case 'delete_field': {
await this.workspaceMetadataFieldMigrationRunnerService.runDeleteFieldMetadataMigration(
{ action, queryRunner },
);
break;
}
case 'create_index': {
await this.workspaceMetadataIndexMigrationRunnerService.runCreateIndexMetadataMigration(
{ action, queryRunner },
);
break;
}
case 'delete_index': {
await this.workspaceMetadataIndexMigrationRunnerService.runDeleteIndexMetadataMigration(
{ action, queryRunner },
);
break;
}
default: {
assertUnreachable(
action,
'Should never occur, encountered an unsupported workspace migration action type',
);
}
}
}
};
}

View File

@ -1,6 +0,0 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class WorkspaceMetadataMigrationRunnerService {
constructor() {}
}

View File

@ -0,0 +1,32 @@
import { Injectable } from '@nestjs/common';
import {
CreateObjectAction,
DeleteObjectAction,
UpdateObjectAction,
WorkspaceMigrationObjectActionTypeV2,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2';
import { RunnerMethodForActionType } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/runner-method-for-action-type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
@Injectable()
export class WorkspaceMetadataObjectActionRunnerService
implements
RunnerMethodForActionType<WorkspaceMigrationObjectActionTypeV2, 'metadata'>
{
runDeleteObjectMetadataMigration = async (
_action: WorkspaceMigrationActionRunnerArgs<DeleteObjectAction>,
) => {
return;
};
runCreateObjectMetadataMigration = async (
_action: WorkspaceMigrationActionRunnerArgs<CreateObjectAction>,
) => {
return;
};
runUpdateObjectMetadataMigration = async (
_action: WorkspaceMigrationActionRunnerArgs<UpdateObjectAction>,
) => {
return;
};
}

View File

@ -1,15 +1,38 @@
import { Module } from '@nestjs/common';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { WorkspaceMetadataMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-metadata-migration-runner/workspace-metadata-migration-runner.service';
import { WorkspaceMetadataFieldActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-metadata-migration-runner/workspace-metadata-field-action-runner.service';
import { WorkspaceMetadataIndexActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-metadata-migration-runner/workspace-metadata-index-action-runner.service';
import { WorkspaceMetadataMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-metadata-migration-runner/workspace-metadata-migration-runner-service';
import { WorkspaceMetadataObjectActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-metadata-migration-runner/workspace-metadata-object-action-runner.service';
import { WorkspaceMigrationRunnerV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-migration-runner-v2.service';
import { WorkspaceSchemaFieldActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/workspace-schema-field-action-runner.service';
import { WorkspaceSchemaIndexActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/workspace-schema-index-action-runner.service';
import { WorkspaceSchemaMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/workspace-schema-migration-runner.service';
import { WorkspaceSchemaObjectActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/workspace-schema-object-action-runner.service';
@Module({
imports: [FeatureFlagModule],
imports: [FeatureFlagModule, TypeORMModule],
providers: [
WorkspaceMetadataObjectActionRunnerService,
WorkspaceMetadataIndexActionRunnerService,
WorkspaceMetadataFieldActionRunnerService,
WorkspaceSchemaObjectActionRunnerService,
WorkspaceSchemaIndexActionRunnerService,
WorkspaceSchemaFieldActionRunnerService,
WorkspaceMetadataMigrationRunnerService,
WorkspaceSchemaMigrationRunnerService,
WorkspaceMigrationRunnerV2Service,
],
exports: [
WorkspaceMigrationRunnerV2Service,
WorkspaceMetadataObjectActionRunnerService,
WorkspaceMetadataIndexActionRunnerService,
WorkspaceMetadataFieldActionRunnerService,
WorkspaceSchemaObjectActionRunnerService,
WorkspaceSchemaIndexActionRunnerService,
WorkspaceSchemaFieldActionRunnerService,
],
exports: [],
})
export class WorkspaceMigrationRunnerV2Module {}

View File

@ -0,0 +1,47 @@
import { Injectable } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { WorkspaceMigrationV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-v2';
import { WorkspaceMetadataMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-metadata-migration-runner/workspace-metadata-migration-runner-service';
import { WorkspaceSchemaMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/workspace-schema-migration-runner.service';
@Injectable()
export class WorkspaceMigrationRunnerV2Service {
constructor(
private readonly workspaceMetadataMigrationRunner: WorkspaceMetadataMigrationRunnerService,
private readonly workspaceSchemaMigrationRunner: WorkspaceSchemaMigrationRunnerService,
@InjectDataSource('core')
private readonly coreDataSource: DataSource,
) {}
run = async (workspaceMigration: WorkspaceMigrationV2) => {
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
await Promise.all([
this.workspaceMetadataMigrationRunner.runWorkspaceMetadataMigration({
queryRunner,
workspaceMigration,
}),
this.workspaceSchemaMigrationRunner.runWorkspaceSchemaMigration({
queryRunner,
workspaceMigration,
}),
]);
await queryRunner.commitTransaction();
} catch (error) {
if (queryRunner.isTransactionActive) {
await queryRunner.rollbackTransaction();
}
throw error;
} finally {
await queryRunner.release();
}
};
}

View File

@ -0,0 +1,32 @@
import { Injectable } from '@nestjs/common';
import {
CreateFieldAction,
DeleteFieldAction,
UpdateFieldAction,
WorkspaceMigrationFieldActionTypeV2,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2';
import { RunnerMethodForActionType } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/runner-method-for-action-type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
@Injectable()
export class WorkspaceSchemaFieldActionRunnerService
implements
RunnerMethodForActionType<WorkspaceMigrationFieldActionTypeV2, 'schema'>
{
runDeleteFieldSchemaMigration = async (
_action: WorkspaceMigrationActionRunnerArgs<DeleteFieldAction>,
) => {
return;
};
runCreateFieldSchemaMigration = async (
_action: WorkspaceMigrationActionRunnerArgs<CreateFieldAction>,
) => {
return;
};
runUpdateFieldSchemaMigration = async (
_action: WorkspaceMigrationActionRunnerArgs<UpdateFieldAction>,
) => {
return;
};
}

View File

@ -0,0 +1,26 @@
import { Injectable } from '@nestjs/common';
import {
CreateIndexAction,
DeleteIndexAction,
WorkspaceMigrationIndexActionTypeV2,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-index-action-v2';
import { RunnerMethodForActionType } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/runner-method-for-action-type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
@Injectable()
export class WorkspaceSchemaIndexActionRunnerService
implements
RunnerMethodForActionType<WorkspaceMigrationIndexActionTypeV2, 'schema'>
{
runDeleteIndexSchemaMigration = async (
_action: WorkspaceMigrationActionRunnerArgs<DeleteIndexAction>,
) => {
return;
};
runCreateIndexSchemaMigration = async (
_action: WorkspaceMigrationActionRunnerArgs<CreateIndexAction>,
) => {
return;
};
}

View File

@ -1,6 +1,81 @@
import { Injectable } from '@nestjs/common';
import { assertUnreachable } from 'twenty-shared/utils';
import { WorkspaceMigrationRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-runner-args.type';
import { WorkspaceSchemaFieldActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/workspace-schema-field-action-runner.service';
import { WorkspaceSchemaIndexActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/workspace-schema-index-action-runner.service';
import { WorkspaceSchemaObjectActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/workspace-schema-object-action-runner.service';
@Injectable()
export class WorkspaceSchemaMigrationRunnerService {
constructor() {}
constructor(
private readonly workspaceSchemaObjectMigrationRunnerService: WorkspaceSchemaObjectActionRunnerService,
private readonly workspaceSchemaIndexMigrationRunnerService: WorkspaceSchemaIndexActionRunnerService,
private readonly workspaceSchemaFieldMigrationRunnerService: WorkspaceSchemaFieldActionRunnerService,
) {}
runWorkspaceSchemaMigration = async ({
workspaceMigration,
queryRunner,
}: WorkspaceMigrationRunnerArgs) => {
for (const action of workspaceMigration.actions) {
switch (action.type) {
case 'delete_object': {
await this.workspaceSchemaObjectMigrationRunnerService.runDeleteObjectSchemaMigration(
{ action, queryRunner },
);
break;
}
case 'create_object': {
await this.workspaceSchemaObjectMigrationRunnerService.runCreateObjectSchemaMigration(
{ action, queryRunner },
);
break;
}
case 'update_object': {
await this.workspaceSchemaObjectMigrationRunnerService.runUpdateObjectSchemaMigration(
{ action, queryRunner },
);
break;
}
case 'create_field': {
await this.workspaceSchemaFieldMigrationRunnerService.runCreateFieldSchemaMigration(
{ action, queryRunner },
);
break;
}
case 'update_field': {
await this.workspaceSchemaFieldMigrationRunnerService.runUpdateFieldSchemaMigration(
{ action, queryRunner },
);
break;
}
case 'delete_field': {
await this.workspaceSchemaFieldMigrationRunnerService.runDeleteFieldSchemaMigration(
{ action, queryRunner },
);
break;
}
case 'create_index': {
await this.workspaceSchemaIndexMigrationRunnerService.runCreateIndexSchemaMigration(
{ action, queryRunner },
);
break;
}
case 'delete_index': {
await this.workspaceSchemaIndexMigrationRunnerService.runDeleteIndexSchemaMigration(
{ action, queryRunner },
);
break;
}
default: {
assertUnreachable(
action,
'Should never occur, encountered an unsupported workspace migration action type',
);
}
}
}
};
}

View File

@ -0,0 +1,32 @@
import { Injectable } from '@nestjs/common';
import {
CreateObjectAction,
DeleteObjectAction,
UpdateObjectAction,
WorkspaceMigrationObjectActionTypeV2,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2';
import { RunnerMethodForActionType } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/runner-method-for-action-type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
@Injectable()
export class WorkspaceSchemaObjectActionRunnerService
implements
RunnerMethodForActionType<WorkspaceMigrationObjectActionTypeV2, 'schema'>
{
runDeleteObjectSchemaMigration = async (
_action: WorkspaceMigrationActionRunnerArgs<DeleteObjectAction>,
) => {
return;
};
runCreateObjectSchemaMigration = async (
_action: WorkspaceMigrationActionRunnerArgs<CreateObjectAction>,
) => {
return;
};
runUpdateObjectSchemaMigration = async (
_action: WorkspaceMigrationActionRunnerArgs<UpdateObjectAction>,
) => {
return;
};
}

View File

@ -9,6 +9,6 @@ import { WorkspaceMigrationRunnerV2Module } from 'src/engine/workspace-manager/w
WorkspaceMigrationRunnerV2Module,
],
providers: [],
exports: [],
exports: [WorkspaceMigrationRunnerV2Module],
})
export class WorkspaceMigrationV2Module {}