Compare commits

...

2 Commits

Author SHA1 Message Date
Weiko 1ff2e3d990 snapshot 2026-02-11 11:25:28 +01:00
Weiko 822637da1f Attach file entity to front-component 2026-02-11 10:48:49 +01:00
23 changed files with 183 additions and 8 deletions
@@ -1001,6 +1001,7 @@ export type CreateFrontComponentInput = {
builtComponentPath: Scalars['String'];
componentName: Scalars['String'];
description?: InputMaybe<Scalars['String']>;
fileId?: InputMaybe<Scalars['UUID']>;
id?: InputMaybe<Scalars['UUID']>;
name: Scalars['String'];
sourceComponentPath: Scalars['String'];
@@ -1673,6 +1674,7 @@ export type FrontComponent = {
componentName: Scalars['String'];
createdAt: Scalars['DateTime'];
description?: Maybe<Scalars['String']>;
fileId?: Maybe<Scalars['UUID']>;
id: Scalars['UUID'];
name: Scalars['String'];
sourceComponentPath: Scalars['String'];
@@ -4713,6 +4715,7 @@ export type UpdateFrontComponentInput = {
export type UpdateFrontComponentInputUpdates = {
description?: InputMaybe<Scalars['String']>;
fileId?: InputMaybe<Scalars['UUID']>;
name?: InputMaybe<Scalars['String']>;
};
@@ -997,6 +997,7 @@ export type CreateFrontComponentInput = {
builtComponentPath: Scalars['String'];
componentName: Scalars['String'];
description?: InputMaybe<Scalars['String']>;
fileId?: InputMaybe<Scalars['UUID']>;
id?: InputMaybe<Scalars['UUID']>;
name: Scalars['String'];
sourceComponentPath: Scalars['String'];
@@ -1645,6 +1646,7 @@ export type FrontComponent = {
componentName: Scalars['String'];
createdAt: Scalars['DateTime'];
description?: Maybe<Scalars['String']>;
fileId?: Maybe<Scalars['UUID']>;
id: Scalars['UUID'];
name: Scalars['String'];
sourceComponentPath: Scalars['String'];
@@ -4551,6 +4553,7 @@ export type UpdateFrontComponentInput = {
export type UpdateFrontComponentInputUpdates = {
description?: InputMaybe<Scalars['String']>;
fileId?: InputMaybe<Scalars['UUID']>;
name?: InputMaybe<Scalars['String']>;
};
@@ -525,7 +525,7 @@ export class ApiService {
builtHandlerPath: string;
fileFolder: FileFolder;
applicationUniversalIdentifier: string;
}): Promise<ApiResponse<boolean>> {
}): Promise<ApiResponse<{ id: string; path: string }>> {
try {
const absolutePath = path.resolve(filePath);
@@ -543,7 +543,7 @@ export class ApiService {
const mutation = `
mutation UploadApplicationFile($file: Upload!, $applicationUniversalIdentifier: String!, $fileFolder: FileFolder!, $filePath: String!) {
uploadApplicationFile(file: $file, applicationUniversalIdentifier: $applicationUniversalIdentifier, fileFolder: $fileFolder, filePath: $filePath)
{ path }
{ id path }
}
`;
@@ -14,7 +14,12 @@ export type UpdateManifestChecksumParams = {
manifest: Manifest;
builtFileInfos: Map<
string,
{ checksum: string; builtPath: string; fileFolder: FileFolder }
{
checksum: string;
builtPath: string;
fileFolder: FileFolder;
fileId?: string;
}
>;
};
@@ -25,7 +30,7 @@ export const manifestUpdateChecksums = ({
let result = structuredClone(manifest);
for (const [
builtPath,
{ fileFolder, checksum },
{ fileFolder, checksum, fileId },
] of builtFileInfos.entries()) {
const rootBuiltPath = relative(OUTPUT_DIR, builtPath);
if (fileFolder === FileFolder.BuiltLogicFunction) {
@@ -72,7 +77,11 @@ export const manifestUpdateChecksums = ({
...result,
frontComponents: frontComponents.map((component, index) =>
index === componentIndex
? { ...component, builtComponentChecksum: checksum }
? {
...component,
builtComponentChecksum: checksum,
...(fileId ? { fileId } : {}),
}
: component,
),
};
@@ -30,6 +30,7 @@ export class DevModeOrchestrator {
builtPath: string;
sourcePath: string;
fileFolder: FileFolder;
fileId?: string;
}
>();
@@ -175,6 +176,12 @@ export class DevModeOrchestrator {
})
.then((result) => {
if (result.success) {
const existingInfo = this.builtFileInfos.get(builtPath);
if (existingInfo) {
existingInfo.fileId = result.data.id;
}
this.uiStateManager.addEvent({
message: `Successfully uploaded ${builtPath}`,
status: 'success',
@@ -0,0 +1,31 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class AddFileRelationToFrontComponent1770742116157
implements MigrationInterface
{
name = 'AddFileRelationToFrontComponent1770742116157';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."frontComponent" ADD "fileId" uuid`,
);
await queryRunner.query(
`ALTER TABLE "core"."frontComponent" ADD CONSTRAINT "UQ_fd86790edb444e6f6e36e021b64" UNIQUE ("fileId")`,
);
await queryRunner.query(
`ALTER TABLE "core"."frontComponent" ADD CONSTRAINT "FK_fd86790edb444e6f6e36e021b64" FOREIGN KEY ("fileId") REFERENCES "core"."file"("id") ON DELETE RESTRICT ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."frontComponent" DROP CONSTRAINT "FK_fd86790edb444e6f6e36e021b64"`,
);
await queryRunner.query(
`ALTER TABLE "core"."frontComponent" DROP CONSTRAINT "UQ_fd86790edb444e6f6e36e021b64"`,
);
await queryRunner.query(
`ALTER TABLE "core"."frontComponent" DROP COLUMN "fileId"`,
);
}
}
@@ -59,6 +59,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
"sourceComponentPath",
"builtComponentPath",
"componentName",
"fileId",
],
"propertiesToStringify": [],
},
@@ -341,6 +341,7 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
sourceComponentPath: { toStringify: false, universalProperty: undefined },
builtComponentPath: { toStringify: false, universalProperty: undefined },
componentName: { toStringify: false, universalProperty: undefined },
fileId: { toStringify: false, universalProperty: undefined },
},
webhook: {
targetUrl: { toStringify: false, universalProperty: undefined },
@@ -494,6 +494,7 @@ export const ALL_METADATA_RELATIONS = {
manyToOne: {
workspace: null,
application: null,
file: null,
},
oneToMany: {},
},
@@ -7,4 +7,5 @@ export const FLAT_FRONT_COMPONENT_EDITABLE_PROPERTIES = [
'sourceComponentPath',
'builtComponentPath',
'componentName',
'fileId',
] as const satisfies MetadataEntityPropertyName<'frontComponent'>[];
@@ -33,6 +33,7 @@ export const fromCreateFrontComponentInputToFlatFrontComponentToCreate = ({
builtComponentPath: createFrontComponentInput.builtComponentPath,
componentName: createFrontComponentInput.componentName,
builtComponentChecksum: createFrontComponentInput.builtComponentChecksum,
fileId: createFrontComponentInput.fileId ?? null,
workspaceId,
createdAt: now,
updatedAt: now,
@@ -15,6 +15,9 @@ export const fromFlatFrontComponentToFrontComponentDto = (
builtComponentPath: flatFrontComponent.builtComponentPath,
componentName: flatFrontComponent.componentName,
builtComponentChecksum: flatFrontComponent.builtComponentChecksum,
fileId: isDefined(flatFrontComponent.fileId)
? flatFrontComponent.fileId
: undefined,
universalIdentifier: isDefined(flatFrontComponent.universalIdentifier)
? flatFrontComponent.universalIdentifier
: undefined,
@@ -31,6 +31,7 @@ export const fromFrontComponentEntityToFlatFrontComponent = ({
builtComponentPath: frontComponentEntity.builtComponentPath,
componentName: frontComponentEntity.componentName,
builtComponentChecksum: frontComponentEntity.builtComponentChecksum,
fileId: frontComponentEntity.fileId,
workspaceId: frontComponentEntity.workspaceId,
universalIdentifier: frontComponentEntity.universalIdentifier,
applicationId: frontComponentEntity.applicationId,
@@ -44,4 +44,9 @@ export class CreateFrontComponentInput {
@IsNotEmpty()
@Field()
builtComponentChecksum: string;
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
fileId?: string;
}
@@ -43,6 +43,11 @@ export class FrontComponentDTO {
@Field()
builtComponentChecksum: string;
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
fileId?: string;
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
@@ -27,6 +27,11 @@ export class UpdateFrontComponentInputUpdates {
@IsString()
@HideField()
builtComponentChecksum?: string;
@IsOptional()
@IsUUID()
@Field(() => UUIDScalarType, { nullable: true })
fileId?: string;
}
@InputType()
@@ -2,10 +2,14 @@ import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
OneToOne,
PrimaryGeneratedColumn,
Relation,
UpdateDateColumn,
} from 'typeorm';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
@Entity('frontComponent')
@@ -34,6 +38,13 @@ export class FrontComponentEntity
@Column({ nullable: false })
builtComponentChecksum: string;
@Column({ nullable: true, type: 'uuid' })
fileId: string | null;
@OneToOne(() => FileEntity, { onDelete: 'RESTRICT' })
@JoinColumn({ name: 'fileId' })
file: Relation<FileEntity> | null;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@@ -1,6 +1,9 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { FlatFrontComponentModule } from 'src/engine/metadata-modules/flat-front-component/flat-front-component.module';
import { FrontComponentController } from 'src/engine/metadata-modules/front-component/controllers/front-component.controller';
@@ -14,6 +17,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
@Module({
imports: [
TypeOrmModule.forFeature([FileEntity, ApplicationEntity]),
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceMigrationModule,
ApplicationModule,
@@ -1,13 +1,18 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { type Readable } from 'stream';
import { FileFolder } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { removeFileFolderFromFileEntityPath } from 'src/engine/core-modules/file/utils/remove-file-folder-from-file-entity-path.utils';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
@@ -32,6 +37,10 @@ export class FrontComponentService {
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly applicationService: ApplicationService,
private readonly fileStorageService: FileStorageService,
@InjectRepository(FileEntity)
private readonly fileRepository: Repository<FileEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
) {}
async findAll(workspaceId: string): Promise<FrontComponentDTO[]> {
@@ -300,6 +309,13 @@ export class FrontComponentService {
workspaceId,
);
if (isDefined(frontComponent.fileId)) {
return this.getFileStreamByFileId({
fileId: frontComponent.fileId,
workspaceId,
});
}
const application = await this.applicationService.findOneApplicationOrThrow(
{
id: frontComponent.applicationId,
@@ -314,4 +330,34 @@ export class FrontComponentService {
resourcePath: frontComponent.builtComponentPath,
});
}
private async getFileStreamByFileId({
fileId,
workspaceId,
}: {
fileId: string;
workspaceId: string;
}): Promise<Readable> {
const file = await this.fileRepository.findOneOrFail({
where: {
id: fileId,
workspaceId,
application: { workspaceId },
},
});
const application = await this.applicationRepository.findOneOrFail({
where: {
id: file.applicationId,
workspaceId,
},
});
return this.fileStorageService.readFile({
resourcePath: removeFileFolderFromFileEntityPath(file.path),
fileFolder: FileFolder.BuiltFrontComponent,
applicationUniversalIdentifier: application.universalIdentifier,
workspaceId,
});
}
}
@@ -504,6 +504,7 @@ export const ALL_UNIVERSAL_METADATA_RELATIONS = {
manyToOne: {
workspace: null,
application: null,
file: null,
},
oneToMany: {},
},
@@ -1,12 +1,15 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { FileFolder } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
import {
FrontComponentException,
@@ -26,7 +29,11 @@ export class CreateFrontComponentActionHandlerService extends WorkspaceMigration
'create',
'frontComponent',
) {
constructor(private readonly fileStorageService: FileStorageService) {
constructor(
private readonly fileStorageService: FileStorageService,
@InjectRepository(FileEntity)
private readonly fileRepository: Repository<FileEntity>,
) {
super();
}
@@ -54,7 +61,12 @@ export class CreateFrontComponentActionHandlerService extends WorkspaceMigration
const applicationUniversalIdentifier = flatApplication.universalIdentifier;
if (isDefined(frontComponent.builtComponentChecksum)) {
if (isDefined(frontComponent.fileId)) {
await this.verifyFileExistsByFileId({
fileId: frontComponent.fileId,
workspaceId,
});
} else if (isDefined(frontComponent.builtComponentChecksum)) {
await this.verifySourceAndBuiltFilesExist({
workspaceId,
applicationUniversalIdentifier,
@@ -97,6 +109,28 @@ export class CreateFrontComponentActionHandlerService extends WorkspaceMigration
}
}
private async verifyFileExistsByFileId({
fileId,
workspaceId,
}: {
fileId: string;
workspaceId: string;
}): Promise<void> {
const file = await this.fileRepository.findOne({
where: {
id: fileId,
workspaceId,
},
});
if (!isDefined(file)) {
throw new FrontComponentException(
`Front component file with id ${fileId} not found`,
FrontComponentExceptionCode.FRONT_COMPONENT_CREATE_FAILED,
);
}
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerContext<FlatCreateFrontComponentAction>,
): Promise<void> {
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
import { CreateAgentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/agent/services/create-agent-action-handler.service';
import { DeleteAgentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/agent/services/delete-agent-action-handler.service';
@@ -72,7 +73,7 @@ import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/wor
@Module({
imports: [
TypeOrmModule.forFeature([ApplicationEntity]),
TypeOrmModule.forFeature([ApplicationEntity, FileEntity]),
WorkspaceSchemaManagerModule,
],
providers: [
@@ -6,4 +6,5 @@ export type FrontComponentManifest = {
builtComponentPath: string;
builtComponentChecksum: string;
componentName: string;
fileId?: string;
};