Fix front data model edition + non nullable workspaceCustom application migration (#16016)
# Introduction Two things: - Enforcing non nullable workspace custom application Id for any workspace - Fixing front non editable data models following https://github.com/twentyhq/twenty/pull/15911 that associate any custom entities to an applicationId. The front was putting everything as readonly when under an app ( we will have to handle the twenty standard application in the future too ) ## Fallback ### Migration The non nullable migration will fail when released, that's why it's being swallowed and re-run in an upgrade command post workspace custom application creation for those that miss one. Allowing the migration to pass in the end The typeorm migration still need to exists for any new workspaces ### GetCurrentUser In order to dynamically display isReadOnly in data model settings we're fetching the workspaceCustomApplicationId through the `getCurrentUser` If not fallback this endpoint would throw until we're handling existing workspaces that do not have a custom workspace application The fallback should be removed post release
This commit is contained in:
@@ -44,6 +44,7 @@ export class ApplicationEntity {
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
description: string | null;
|
||||
|
||||
// TODO should not be nullable
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
version: string | null;
|
||||
|
||||
|
||||
@@ -27,15 +27,21 @@ export class ApplicationService {
|
||||
) {}
|
||||
|
||||
async findWorkspaceTwentyStandardAndCustomApplicationOrThrow({
|
||||
workspace: workspaceInput,
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}) {
|
||||
const workspace = await this.workspaceRepository.findOne({
|
||||
where: {
|
||||
id: workspaceId,
|
||||
},
|
||||
});
|
||||
}:
|
||||
| {
|
||||
workspaceId: string;
|
||||
workspace?: never;
|
||||
}
|
||||
| { workspace: WorkspaceEntity; workspaceId?: never }) {
|
||||
const workspace = isDefined(workspaceInput)
|
||||
? workspaceInput
|
||||
: await this.workspaceRepository.findOne({
|
||||
where: {
|
||||
id: workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(workspace)) {
|
||||
throw new ApplicationException(
|
||||
@@ -47,7 +53,7 @@ export class ApplicationService {
|
||||
const flatApplicationMaps =
|
||||
await this.workspaceFlatApplicationMapCacheService.getExistingOrRecomputeFlatMaps(
|
||||
{
|
||||
workspaceId,
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
);
|
||||
const twentyStandardApplicationId =
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { IsBoolean, IsNotEmpty, IsString, IsUUID } from 'class-validator';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { ApplicationVariableEntityDTO } from 'src/engine/core-modules/applicationVariable/dtos/application-variable.dto';
|
||||
@@ -19,13 +25,15 @@ export class ApplicationDTO {
|
||||
@Field()
|
||||
name: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field()
|
||||
description: string;
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field()
|
||||
version: string;
|
||||
version?: string;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
@@ -36,14 +44,14 @@ export class ApplicationDTO {
|
||||
canBeUninstalled: boolean;
|
||||
|
||||
@Field(() => [AgentDTO])
|
||||
agents: AgentDTO[];
|
||||
agents?: AgentDTO[];
|
||||
|
||||
@Field(() => [ServerlessFunctionDTO])
|
||||
serverlessFunctions: ServerlessFunctionDTO[];
|
||||
serverlessFunctions?: ServerlessFunctionDTO[];
|
||||
|
||||
@Field(() => [ObjectMetadataDTO])
|
||||
objects: ObjectMetadataDTO[];
|
||||
objects?: ObjectMetadataDTO[];
|
||||
|
||||
@Field(() => [ApplicationVariableEntityDTO])
|
||||
applicationVariables: ApplicationVariableEntityDTO[];
|
||||
applicationVariables?: ApplicationVariableEntityDTO[];
|
||||
}
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { type ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto';
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
|
||||
export const fromFlatApplicationToApplicationDto = ({
|
||||
canBeUninstalled,
|
||||
|
||||
description,
|
||||
id,
|
||||
name,
|
||||
|
||||
universalIdentifier,
|
||||
version,
|
||||
}: FlatApplication): ApplicationDTO => {
|
||||
return {
|
||||
canBeUninstalled,
|
||||
description: description ?? undefined,
|
||||
id,
|
||||
name,
|
||||
objects: [],
|
||||
universalIdentifier,
|
||||
version: version ?? undefined,
|
||||
};
|
||||
};
|
||||
@@ -23,6 +23,7 @@ import { ModelId } from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||
import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
import { AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto';
|
||||
import { ApprovedAccessDomainEntity } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.entity';
|
||||
import { EmailingDomainEntity } from 'src/engine/core-modules/emailing-domain/emailing-domain.entity';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
@@ -286,12 +287,10 @@ export class WorkspaceEntity {
|
||||
@Column({ type: 'varchar', nullable: false, default: 'auto' })
|
||||
routerModel: ModelId;
|
||||
|
||||
// TODO prastoin
|
||||
// Temporarily setting as nullable for retro compatibility, not udpating TypeScript types
|
||||
@Field(() => String, { nullable: true })
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
@Column({ nullable: false, type: 'uuid' })
|
||||
workspaceCustomApplicationId: string;
|
||||
|
||||
@Field(() => ApplicationDTO, { nullable: true })
|
||||
@ManyToOne(() => ApplicationEntity, {
|
||||
onDelete: 'RESTRICT',
|
||||
nullable: false,
|
||||
|
||||
@@ -28,6 +28,7 @@ import { WorkspaceWorkspaceMemberListener } from 'src/engine/core-modules/worksp
|
||||
import { workspaceAutoResolverOpts } from 'src/engine/core-modules/workspace/workspace.auto-resolver-opts';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceResolver } from 'src/engine/core-modules/workspace/workspace.resolver';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { AgentModule } from 'src/engine/metadata-modules/agent/agent.module';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
@@ -72,6 +73,7 @@ import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-m
|
||||
CustomDomainManagerModule,
|
||||
ViewModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
ApplicationModule,
|
||||
],
|
||||
services: [WorkspaceService],
|
||||
resolvers: workspaceAutoResolverOpts,
|
||||
|
||||
@@ -23,6 +23,9 @@ import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.
|
||||
|
||||
import type { FileUpload } from 'graphql-upload/processRequest.mjs';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto';
|
||||
import { fromFlatApplicationToApplicationDto } from 'src/engine/core-modules/application/utils/from-flat-application-to-application-dto.util';
|
||||
import { BillingSubscriptionEntity } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||
import { DomainValidRecords } from 'src/engine/core-modules/dns-manager/dtos/domain-valid-records';
|
||||
@@ -103,6 +106,7 @@ export class WorkspaceResolver {
|
||||
private readonly viewService: ViewService,
|
||||
private readonly dnsManagerService: DnsManagerService,
|
||||
private readonly customDomainManagerService: CustomDomainManagerService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
) {}
|
||||
|
||||
@Query(() => WorkspaceEntity)
|
||||
@@ -240,6 +244,27 @@ export class WorkspaceResolver {
|
||||
return workspace.routerModel;
|
||||
}
|
||||
|
||||
@ResolveField(() => ApplicationDTO, { nullable: true })
|
||||
async workspaceCustomApplication(
|
||||
@Parent() workspace: WorkspaceEntity,
|
||||
): Promise<ApplicationDTO | null> {
|
||||
try {
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{
|
||||
workspace,
|
||||
},
|
||||
);
|
||||
|
||||
return fromFlatApplicationToApplicationDto(
|
||||
workspaceCustomFlatApplication,
|
||||
);
|
||||
} catch {
|
||||
// Temporary should be removed after CreateWorkspaceCustomApplicationCommand is run
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ResolveField(() => BillingSubscriptionEntity, { nullable: true })
|
||||
async currentBillingSubscription(
|
||||
@Parent() workspace: WorkspaceEntity,
|
||||
|
||||
Reference in New Issue
Block a user