Fix upgrade commands discovery outside of cli (#19671)

# Introduction
We were allowing the sequence to be empty in the worker context that was
facing an edge case importing the UpgradeModule through the
WorkspaceModule god module, no commands were discovered and it was
throwing as the sequence must have at least one workspace commands to
allow a workspace creation

Though the issue was also applicable to the twenty-server `AppModule`
too that was not discovering any commands

## Integration tests were passing
The integration test were importing the `CommandModule` at the nest
testing app creating leading to asymmetric testing context
It was a requirement for a legacy commands import and global assignation

## Fix
The `UpgradeModule` now import both `WorkspaceCommandsProviderModule`
and `InstanceCommandProviderModule` which ships the commands directly in
the module
We could consider moving the commands into the `engine/upgrade` folder

## Concern
Bootstrap could become more and more long to load at both server and
worker start
When this becomes a problem we will have to only import the latest
workspace command or whatever
For the moment this is not worth it the risk to import not the latest
workspace command
This commit is contained in:
Paul Rastoin
2026-04-14 11:20:33 +02:00
committed by GitHub
parent f738961127
commit 3e699c4458
12 changed files with 372 additions and 399 deletions
@@ -2193,30 +2193,18 @@ type DeletedWorkspaceMember {
userWorkspaceId: UUID
}
type BillingEntitlement {
key: BillingEntitlementKey!
value: Boolean!
type Relation {
type: RelationType!
sourceObjectMetadata: Object!
targetObjectMetadata: Object!
sourceFieldMetadata: Field!
targetFieldMetadata: Field!
}
enum BillingEntitlementKey {
SSO
CUSTOM_DOMAIN
RLS
AUDIT_LOGS
}
type DomainRecord {
validationType: String!
type: String!
status: String!
key: String!
value: String!
}
type DomainValidRecords {
id: UUID!
domain: String!
records: [DomainRecord!]!
"""Relation type"""
enum RelationType {
ONE_TO_MANY
MANY_TO_ONE
}
type IndexEdge {
@@ -2318,25 +2306,6 @@ type ObjectFieldsConnection {
edges: [FieldEdge!]!
}
type UpsertRowLevelPermissionPredicatesResult {
predicates: [RowLevelPermissionPredicate!]!
predicateGroups: [RowLevelPermissionPredicateGroup!]!
}
type Relation {
type: RelationType!
sourceObjectMetadata: Object!
targetObjectMetadata: Object!
sourceFieldMetadata: Field!
targetFieldMetadata: Field!
}
"""Relation type"""
enum RelationType {
ONE_TO_MANY
MANY_TO_ONE
}
type FieldConnection {
"""Paging information"""
pageInfo: PageInfo!
@@ -2345,6 +2314,37 @@ type FieldConnection {
edges: [FieldEdge!]!
}
type BillingEntitlement {
key: BillingEntitlementKey!
value: Boolean!
}
enum BillingEntitlementKey {
SSO
CUSTOM_DOMAIN
RLS
AUDIT_LOGS
}
type DomainRecord {
validationType: String!
type: String!
status: String!
key: String!
value: String!
}
type DomainValidRecords {
id: UUID!
domain: String!
records: [DomainRecord!]!
}
type UpsertRowLevelPermissionPredicatesResult {
predicates: [RowLevelPermissionPredicate!]!
predicateGroups: [RowLevelPermissionPredicateGroup!]!
}
type AuthToken {
token: String!
expiresAt: DateTime!
@@ -1879,29 +1879,18 @@ export interface DeletedWorkspaceMember {
__typename: 'DeletedWorkspaceMember'
}
export interface BillingEntitlement {
key: BillingEntitlementKey
value: Scalars['Boolean']
__typename: 'BillingEntitlement'
export interface Relation {
type: RelationType
sourceObjectMetadata: Object
targetObjectMetadata: Object
sourceFieldMetadata: Field
targetFieldMetadata: Field
__typename: 'Relation'
}
export type BillingEntitlementKey = 'SSO' | 'CUSTOM_DOMAIN' | 'RLS' | 'AUDIT_LOGS'
export interface DomainRecord {
validationType: Scalars['String']
type: Scalars['String']
status: Scalars['String']
key: Scalars['String']
value: Scalars['String']
__typename: 'DomainRecord'
}
export interface DomainValidRecords {
id: Scalars['UUID']
domain: Scalars['String']
records: DomainRecord[]
__typename: 'DomainValidRecords'
}
/** Relation type */
export type RelationType = 'ONE_TO_MANY' | 'MANY_TO_ONE'
export interface IndexEdge {
/** The node containing the Index */
@@ -2001,25 +1990,6 @@ export interface ObjectFieldsConnection {
__typename: 'ObjectFieldsConnection'
}
export interface UpsertRowLevelPermissionPredicatesResult {
predicates: RowLevelPermissionPredicate[]
predicateGroups: RowLevelPermissionPredicateGroup[]
__typename: 'UpsertRowLevelPermissionPredicatesResult'
}
export interface Relation {
type: RelationType
sourceObjectMetadata: Object
targetObjectMetadata: Object
sourceFieldMetadata: Field
targetFieldMetadata: Field
__typename: 'Relation'
}
/** Relation type */
export type RelationType = 'ONE_TO_MANY' | 'MANY_TO_ONE'
export interface FieldConnection {
/** Paging information */
pageInfo: PageInfo
@@ -2028,6 +1998,36 @@ export interface FieldConnection {
__typename: 'FieldConnection'
}
export interface BillingEntitlement {
key: BillingEntitlementKey
value: Scalars['Boolean']
__typename: 'BillingEntitlement'
}
export type BillingEntitlementKey = 'SSO' | 'CUSTOM_DOMAIN' | 'RLS' | 'AUDIT_LOGS'
export interface DomainRecord {
validationType: Scalars['String']
type: Scalars['String']
status: Scalars['String']
key: Scalars['String']
value: Scalars['String']
__typename: 'DomainRecord'
}
export interface DomainValidRecords {
id: Scalars['UUID']
domain: Scalars['String']
records: DomainRecord[]
__typename: 'DomainValidRecords'
}
export interface UpsertRowLevelPermissionPredicatesResult {
predicates: RowLevelPermissionPredicate[]
predicateGroups: RowLevelPermissionPredicateGroup[]
__typename: 'UpsertRowLevelPermissionPredicatesResult'
}
export interface AuthToken {
token: Scalars['String']
expiresAt: Scalars['DateTime']
@@ -5149,27 +5149,12 @@ export interface DeletedWorkspaceMemberGenqlSelection{
__scalar?: boolean | number
}
export interface BillingEntitlementGenqlSelection{
key?: boolean | number
value?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface DomainRecordGenqlSelection{
validationType?: boolean | number
export interface RelationGenqlSelection{
type?: boolean | number
status?: boolean | number
key?: boolean | number
value?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface DomainValidRecordsGenqlSelection{
id?: boolean | number
domain?: boolean | number
records?: DomainRecordGenqlSelection
sourceObjectMetadata?: ObjectGenqlSelection
targetObjectMetadata?: ObjectGenqlSelection
sourceFieldMetadata?: FieldGenqlSelection
targetFieldMetadata?: FieldGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
@@ -5284,23 +5269,6 @@ export interface ObjectFieldsConnectionGenqlSelection{
__scalar?: boolean | number
}
export interface UpsertRowLevelPermissionPredicatesResultGenqlSelection{
predicates?: RowLevelPermissionPredicateGenqlSelection
predicateGroups?: RowLevelPermissionPredicateGroupGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface RelationGenqlSelection{
type?: boolean | number
sourceObjectMetadata?: ObjectGenqlSelection
targetObjectMetadata?: ObjectGenqlSelection
sourceFieldMetadata?: FieldGenqlSelection
targetFieldMetadata?: FieldGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface FieldConnectionGenqlSelection{
/** Paging information */
pageInfo?: PageInfoGenqlSelection
@@ -5310,6 +5278,38 @@ export interface FieldConnectionGenqlSelection{
__scalar?: boolean | number
}
export interface BillingEntitlementGenqlSelection{
key?: boolean | number
value?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface DomainRecordGenqlSelection{
validationType?: boolean | number
type?: boolean | number
status?: boolean | number
key?: boolean | number
value?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface DomainValidRecordsGenqlSelection{
id?: boolean | number
domain?: boolean | number
records?: DomainRecordGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface UpsertRowLevelPermissionPredicatesResultGenqlSelection{
predicates?: RowLevelPermissionPredicateGenqlSelection
predicateGroups?: RowLevelPermissionPredicateGroupGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface AuthTokenGenqlSelection{
token?: boolean | number
expiresAt?: boolean | number
@@ -8232,26 +8232,10 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const BillingEntitlement_possibleTypes: string[] = ['BillingEntitlement']
export const isBillingEntitlement = (obj?: { __typename?: any } | null): obj is BillingEntitlement => {
if (!obj?.__typename) throw new Error('__typename is missing in "isBillingEntitlement"')
return BillingEntitlement_possibleTypes.includes(obj.__typename)
}
const DomainRecord_possibleTypes: string[] = ['DomainRecord']
export const isDomainRecord = (obj?: { __typename?: any } | null): obj is DomainRecord => {
if (!obj?.__typename) throw new Error('__typename is missing in "isDomainRecord"')
return DomainRecord_possibleTypes.includes(obj.__typename)
}
const DomainValidRecords_possibleTypes: string[] = ['DomainValidRecords']
export const isDomainValidRecords = (obj?: { __typename?: any } | null): obj is DomainValidRecords => {
if (!obj?.__typename) throw new Error('__typename is missing in "isDomainValidRecords"')
return DomainValidRecords_possibleTypes.includes(obj.__typename)
const Relation_possibleTypes: string[] = ['Relation']
export const isRelation = (obj?: { __typename?: any } | null): obj is Relation => {
if (!obj?.__typename) throw new Error('__typename is missing in "isRelation"')
return Relation_possibleTypes.includes(obj.__typename)
}
@@ -8352,22 +8336,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const UpsertRowLevelPermissionPredicatesResult_possibleTypes: string[] = ['UpsertRowLevelPermissionPredicatesResult']
export const isUpsertRowLevelPermissionPredicatesResult = (obj?: { __typename?: any } | null): obj is UpsertRowLevelPermissionPredicatesResult => {
if (!obj?.__typename) throw new Error('__typename is missing in "isUpsertRowLevelPermissionPredicatesResult"')
return UpsertRowLevelPermissionPredicatesResult_possibleTypes.includes(obj.__typename)
}
const Relation_possibleTypes: string[] = ['Relation']
export const isRelation = (obj?: { __typename?: any } | null): obj is Relation => {
if (!obj?.__typename) throw new Error('__typename is missing in "isRelation"')
return Relation_possibleTypes.includes(obj.__typename)
}
const FieldConnection_possibleTypes: string[] = ['FieldConnection']
export const isFieldConnection = (obj?: { __typename?: any } | null): obj is FieldConnection => {
if (!obj?.__typename) throw new Error('__typename is missing in "isFieldConnection"')
@@ -8376,6 +8344,38 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const BillingEntitlement_possibleTypes: string[] = ['BillingEntitlement']
export const isBillingEntitlement = (obj?: { __typename?: any } | null): obj is BillingEntitlement => {
if (!obj?.__typename) throw new Error('__typename is missing in "isBillingEntitlement"')
return BillingEntitlement_possibleTypes.includes(obj.__typename)
}
const DomainRecord_possibleTypes: string[] = ['DomainRecord']
export const isDomainRecord = (obj?: { __typename?: any } | null): obj is DomainRecord => {
if (!obj?.__typename) throw new Error('__typename is missing in "isDomainRecord"')
return DomainRecord_possibleTypes.includes(obj.__typename)
}
const DomainValidRecords_possibleTypes: string[] = ['DomainValidRecords']
export const isDomainValidRecords = (obj?: { __typename?: any } | null): obj is DomainValidRecords => {
if (!obj?.__typename) throw new Error('__typename is missing in "isDomainValidRecords"')
return DomainValidRecords_possibleTypes.includes(obj.__typename)
}
const UpsertRowLevelPermissionPredicatesResult_possibleTypes: string[] = ['UpsertRowLevelPermissionPredicatesResult']
export const isUpsertRowLevelPermissionPredicatesResult = (obj?: { __typename?: any } | null): obj is UpsertRowLevelPermissionPredicatesResult => {
if (!obj?.__typename) throw new Error('__typename is missing in "isUpsertRowLevelPermissionPredicatesResult"')
return UpsertRowLevelPermissionPredicatesResult_possibleTypes.includes(obj.__typename)
}
const AuthToken_possibleTypes: string[] = ['AuthToken']
export const isAuthToken = (obj?: { __typename?: any } | null): obj is AuthToken => {
if (!obj?.__typename) throw new Error('__typename is missing in "isAuthToken"')
@@ -9576,6 +9576,11 @@ export const enumQueueMetricsTimeRange = {
OneHour: 'OneHour' as const
}
export const enumRelationType = {
ONE_TO_MANY: 'ONE_TO_MANY' as const,
MANY_TO_ONE: 'MANY_TO_ONE' as const
}
export const enumBillingEntitlementKey = {
SSO: 'SSO' as const,
CUSTOM_DOMAIN: 'CUSTOM_DOMAIN' as const,
@@ -9583,11 +9588,6 @@ export const enumBillingEntitlementKey = {
AUDIT_LOGS: 'AUDIT_LOGS' as const
}
export const enumRelationType = {
ONE_TO_MANY: 'ONE_TO_MANY' as const,
MANY_TO_ONE: 'MANY_TO_ONE' as const
}
export const enumEmailingDomainDriver = {
AWS_SES: 'AWS_SES' as const
}
@@ -63,7 +63,7 @@ export default {
209,
221,
238,
255,
253,
292,
293,
303,
@@ -789,10 +789,10 @@ export default {
3
],
"relation": [
254
237
],
"morphRelations": [
254
237
],
"object": [
46
@@ -851,7 +851,7 @@ export default {
36
],
"objectMetadata": [
247,
245,
{
"paging": [
39,
@@ -864,7 +864,7 @@ export default {
}
],
"indexFieldMetadatas": [
245,
243,
{
"paging": [
39,
@@ -1109,7 +1109,7 @@ export default {
37
],
"fields": [
252,
250,
{
"paging": [
39,
@@ -1122,7 +1122,7 @@ export default {
}
],
"indexMetadatas": [
250,
248,
{
"paging": [
39,
@@ -1703,7 +1703,7 @@ export default {
131
],
"billingEntitlements": [
237
252
],
"hasValidEnterpriseKey": [
6
@@ -4419,9 +4419,179 @@ export default {
1
]
},
"Relation": {
"type": [
238
],
"sourceObjectMetadata": [
46
],
"targetObjectMetadata": [
46
],
"sourceFieldMetadata": [
34
],
"targetFieldMetadata": [
34
],
"__typename": [
1
]
},
"RelationType": {},
"IndexEdge": {
"node": [
37
],
"cursor": [
40
],
"__typename": [
1
]
},
"PageInfo": {
"hasNextPage": [
6
],
"hasPreviousPage": [
6
],
"startCursor": [
40
],
"endCursor": [
40
],
"__typename": [
1
]
},
"IndexConnection": {
"pageInfo": [
240
],
"edges": [
239
],
"__typename": [
1
]
},
"IndexFieldEdge": {
"node": [
36
],
"cursor": [
40
],
"__typename": [
1
]
},
"IndexIndexFieldMetadatasConnection": {
"pageInfo": [
240
],
"edges": [
242
],
"__typename": [
1
]
},
"ObjectEdge": {
"node": [
46
],
"cursor": [
40
],
"__typename": [
1
]
},
"IndexObjectMetadataConnection": {
"pageInfo": [
240
],
"edges": [
244
],
"__typename": [
1
]
},
"ObjectRecordCount": {
"objectNamePlural": [
1
],
"totalCount": [
21
],
"__typename": [
1
]
},
"ObjectConnection": {
"pageInfo": [
240
],
"edges": [
244
],
"__typename": [
1
]
},
"ObjectIndexMetadatasConnection": {
"pageInfo": [
240
],
"edges": [
239
],
"__typename": [
1
]
},
"FieldEdge": {
"node": [
34
],
"cursor": [
40
],
"__typename": [
1
]
},
"ObjectFieldsConnection": {
"pageInfo": [
240
],
"edges": [
249
],
"__typename": [
1
]
},
"FieldConnection": {
"pageInfo": [
240
],
"edges": [
249
],
"__typename": [
1
]
},
"BillingEntitlement": {
"key": [
238
253
],
"value": [
6
@@ -4459,145 +4629,7 @@ export default {
1
],
"records": [
239
],
"__typename": [
1
]
},
"IndexEdge": {
"node": [
37
],
"cursor": [
40
],
"__typename": [
1
]
},
"PageInfo": {
"hasNextPage": [
6
],
"hasPreviousPage": [
6
],
"startCursor": [
40
],
"endCursor": [
40
],
"__typename": [
1
]
},
"IndexConnection": {
"pageInfo": [
242
],
"edges": [
241
],
"__typename": [
1
]
},
"IndexFieldEdge": {
"node": [
36
],
"cursor": [
40
],
"__typename": [
1
]
},
"IndexIndexFieldMetadatasConnection": {
"pageInfo": [
242
],
"edges": [
244
],
"__typename": [
1
]
},
"ObjectEdge": {
"node": [
46
],
"cursor": [
40
],
"__typename": [
1
]
},
"IndexObjectMetadataConnection": {
"pageInfo": [
242
],
"edges": [
246
],
"__typename": [
1
]
},
"ObjectRecordCount": {
"objectNamePlural": [
1
],
"totalCount": [
21
],
"__typename": [
1
]
},
"ObjectConnection": {
"pageInfo": [
242
],
"edges": [
246
],
"__typename": [
1
]
},
"ObjectIndexMetadatasConnection": {
"pageInfo": [
242
],
"edges": [
241
],
"__typename": [
1
]
},
"FieldEdge": {
"node": [
34
],
"cursor": [
40
],
"__typename": [
1
]
},
"ObjectFieldsConnection": {
"pageInfo": [
242
],
"edges": [
251
254
],
"__typename": [
1
@@ -4614,38 +4646,6 @@ export default {
1
]
},
"Relation": {
"type": [
255
],
"sourceObjectMetadata": [
46
],
"targetObjectMetadata": [
46
],
"sourceFieldMetadata": [
34
],
"targetFieldMetadata": [
34
],
"__typename": [
1
]
},
"RelationType": {},
"FieldConnection": {
"pageInfo": [
242
],
"edges": [
251
],
"__typename": [
1
]
},
"AuthToken": {
"token": [
1
@@ -5975,7 +5975,7 @@ export default {
},
"AgentChatThreadConnection": {
"pageInfo": [
242
240
],
"edges": [
335
@@ -6582,7 +6582,7 @@ export default {
}
],
"objectRecordCounts": [
248
246
],
"object": [
46,
@@ -6594,7 +6594,7 @@ export default {
}
],
"objects": [
249,
247,
{
"paging": [
39,
@@ -6616,7 +6616,7 @@ export default {
}
],
"indexMetadatas": [
243,
241,
{
"paging": [
39,
@@ -6665,7 +6665,7 @@ export default {
}
],
"fields": [
256,
251,
{
"paging": [
39,
@@ -8340,7 +8340,7 @@ export default {
}
],
"upsertRowLevelPermissionPredicates": [
253,
256,
{
"input": [
456,
@@ -9082,7 +9082,7 @@ export default {
66
],
"checkCustomDomainValidRecords": [
240
255
],
"createOIDCIdentityProvider": [
232,
@@ -9415,7 +9415,7 @@ export default {
}
],
"checkPublicDomainValidRecords": [
240,
255,
{
"domain": [
1,
-2
View File
@@ -3,7 +3,6 @@ import { type INestApplication } from '@nestjs/common';
import 'jest';
import { type DataSource } from 'typeorm';
import { type DataSeedWorkspaceCommand } from 'src/database/commands/data-seed-dev-workspace.command';
import { type DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
declare module '@jest/types' {
@@ -36,7 +35,6 @@ declare global {
var testDataSource: DataSource;
var app: INestApplication;
var dataSourceService: DataSourceService;
var dataSeedWorkspaceCommand: DataSeedWorkspaceCommand;
}
export {};
@@ -8,7 +8,6 @@ import { InstanceCommandGenerationService } from 'src/database/commands/instance
import { ListOrphanedWorkspaceEntitiesCommand } from 'src/database/commands/list-and-delete-orphaned-workspace-entities.command';
import { ConfirmationQuestion } from 'src/database/commands/questions/confirmation.question';
import { RunInstanceCommandsCommand } from 'src/database/commands/run-instance-commands.command';
import { InstanceCommandProviderModule } from 'src/database/commands/upgrade-version-command/instance-command-provider.module';
import { UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/upgrade-version-command.module';
import { WorkspaceExportModule } from 'src/database/commands/workspace-export/workspace-export.module';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
@@ -45,7 +44,6 @@ import { AutomatedTriggerModule } from 'src/modules/workflow/workflow-trigger/au
@Module({
imports: [
UpgradeVersionCommandModule,
InstanceCommandProviderModule,
TypeOrmModule.forFeature([WorkspaceEntity]),
WorkspaceExportModule,
// Cron command dependencies
@@ -1,22 +1,12 @@
import { Module } from '@nestjs/common';
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
import { V1_21_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-21/1-21-upgrade-version-command.module';
import { V1_22_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-22/1-22-upgrade-version-command.module';
import { V1_23_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-23/1-23-upgrade-version-command.module';
import { UpgradeCommand } from 'src/database/commands/upgrade-version-command/upgrade.command';
import { UpgradeModule } from 'src/engine/core-modules/upgrade/upgrade.module';
import { WorkspaceVersionModule } from 'src/engine/workspace-manager/workspace-version/workspace-version.module';
@Module({
imports: [
V1_21_UpgradeVersionCommandModule,
V1_22_UpgradeVersionCommandModule,
V1_23_UpgradeVersionCommandModule,
UpgradeModule,
WorkspaceVersionModule,
WorkspaceIteratorModule,
],
imports: [UpgradeModule, WorkspaceVersionModule, WorkspaceIteratorModule],
providers: [UpgradeCommand],
})
export class UpgradeVersionCommandModule {}
@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { V1_21_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-21/1-21-upgrade-version-command.module';
import { V1_22_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-22/1-22-upgrade-version-command.module';
import { V1_23_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-23/1-23-upgrade-version-command.module';
@Module({
imports: [
V1_21_UpgradeVersionCommandModule,
V1_22_UpgradeVersionCommandModule,
V1_23_UpgradeVersionCommandModule,
],
})
export class WorkspaceCommandProviderModule {}
@@ -157,24 +157,14 @@ describe('UpgradeCommandRegistryService', () => {
);
});
it('should return empty array for version with no commands', async () => {
const service = await buildRegistryService([]);
const bundleB = service.getBundleForVersion(VERSION_B);
const bundleA = service.getBundleForVersion(VERSION_A);
expect(bundleB.fastInstanceCommands).toStrictEqual([]);
expect(bundleA.fastInstanceCommands).toStrictEqual([]);
expect(bundleB.workspaceCommands).toStrictEqual([]);
expect(bundleA.workspaceCommands).toStrictEqual([]);
});
it('should not throw when no commands are discovered (empty bundle)', async () => {
await expect(buildRegistryService([])).resolves.toBeDefined();
it('should throw when no workspace commands are discovered', async () => {
await expect(buildRegistryService([])).rejects.toThrow(
'Upgrade sequence must contain at least one workspace command',
);
});
it('should return empty array for unsupported version', async () => {
const service = await buildRegistryService([]);
const service = await buildRegistryService([new WorkspaceCommandA()]);
expect(
service.getBundleForVersion('99.0.0' as typeof VERSION_A)
@@ -284,8 +274,8 @@ describe('UpgradeCommandRegistryService', () => {
]);
});
it('should return empty array from getCrossUpgradeSupportedFastInstanceCommands when no commands registered', async () => {
const service = await buildRegistryService([]);
it('should return empty array from getCrossUpgradeSupportedFastInstanceCommands when no instance commands registered', async () => {
const service = await buildRegistryService([new WorkspaceCommandA()]);
expect(
service.getCrossUpgradeSupportedFastInstanceCommands(),
@@ -247,33 +247,16 @@ export class UpgradeCommandRegistryService implements OnModuleInit {
}
private validateAtLeastOneVersionBundleHasWorkspaceCommands(): void {
let totalCommandCount = 0;
let hasWorkspaceCommands = false;
for (const version of TWENTY_CROSS_UPGRADE_SUPPORTED_VERSIONS) {
const bundle = this.getBundleForVersion(version);
totalCommandCount +=
bundle.fastInstanceCommands.length +
bundle.slowInstanceCommands.length +
bundle.workspaceCommands.length;
if (bundle.workspaceCommands.length > 0) {
hasWorkspaceCommands = true;
}
}
// UpgradeModule is loaded in the worker transitively via WorkspaceModule,
// but no command modules are imported — zero providers are discovered.
// TODO: split WorkspaceModule so the worker doesn't pull in UpgradeModule
if (totalCommandCount === 0) {
this.logger.warn(
'No upgrade commands discovered — skipping workspace command validation',
);
return;
}
if (!hasWorkspaceCommands) {
throw new Error(
'Upgrade sequence must contain at least one workspace command',
@@ -3,6 +3,8 @@ import { DiscoveryModule } from '@nestjs/core';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
import { InstanceCommandProviderModule } from 'src/database/commands/upgrade-version-command/instance-command-provider.module';
import { WorkspaceCommandProviderModule } from 'src/database/commands/upgrade-version-command/workspace-command-provider.module';
import { InstanceCommandRunnerService } from 'src/engine/core-modules/upgrade/services/instance-command-runner.service';
import { UpgradeCommandRegistryService } from 'src/engine/core-modules/upgrade/services/upgrade-command-registry.service';
import { UpgradeMigrationService } from 'src/engine/core-modules/upgrade/services/upgrade-migration.service';
@@ -16,6 +18,8 @@ import { WorkspaceVersionModule } from 'src/engine/workspace-manager/workspace-v
@Module({
imports: [
DiscoveryModule,
InstanceCommandProviderModule,
WorkspaceCommandProviderModule,
WorkspaceIteratorModule,
WorkspaceVersionModule,
TypeOrmModule.forFeature([UpgradeMigrationEntity, WorkspaceEntity]),
@@ -10,7 +10,6 @@ import bytes from 'bytes';
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import { AppModule } from 'src/app.module';
import { CommandModule } from 'src/command/command.module';
import { settings } from 'src/engine/constants/settings';
import { StripeSDKMockService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/mocks/stripe-sdk-mock.service';
import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service';
@@ -52,7 +51,6 @@ export const createApp = async (
let moduleBuilder: TestingModuleBuilder = Test.createTestingModule({
imports: [
AppModule,
CommandModule,
JobsModule,
MessageQueueModule.registerExplorer(),
],
@@ -1,7 +1,6 @@
import { type JestConfigWithTsJest } from 'ts-jest';
import 'tsconfig-paths/register';
import { DataSeedWorkspaceCommand } from 'src/database/commands/data-seed-dev-workspace.command';
import { rawDataSource } from 'src/database/typeorm/raw/raw.datasource';
import { createApp } from './create-app';
@@ -19,5 +18,4 @@ export default async (_: unknown, projectConfig: JestConfigWithTsJest) => {
global.app = app;
global.testDataSource = rawDataSource;
global.dataSeedWorkspaceCommand = app.get(DataSeedWorkspaceCommand);
};