diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 2f1614ac2f..236a51bbfd 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1497,11 +1497,9 @@ export type FieldsConfiguration = { export type File = { __typename?: 'File'; createdAt: Scalars['DateTime']; - fullPath: Scalars['String']; id: Scalars['UUID']; - name: Scalars['String']; + path: Scalars['String']; size: Scalars['Float']; - type: Scalars['String']; }; export enum FileFolder { @@ -5761,14 +5759,14 @@ export type CreateFileMutationVariables = Exact<{ }>; -export type CreateFileMutation = { __typename?: 'Mutation', createFile: { __typename?: 'File', id: string, name: string, fullPath: string, size: number, type: string, createdAt: string } }; +export type CreateFileMutation = { __typename?: 'Mutation', createFile: { __typename?: 'File', id: string, path: string, size: number, createdAt: string } }; export type DeleteFileMutationVariables = Exact<{ fileId: Scalars['UUID']; }>; -export type DeleteFileMutation = { __typename?: 'Mutation', deleteFile: { __typename?: 'File', id: string, name: string, fullPath: string, size: number, type: string, createdAt: string } }; +export type DeleteFileMutation = { __typename?: 'Mutation', deleteFile: { __typename?: 'File', id: string, path: string, size: number, createdAt: string } }; export type ObjectMetadataFieldsFragment = { __typename?: 'Object', id: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, applicationId?: string | null, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, applicationId?: string | null, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> }; @@ -9932,10 +9930,8 @@ export const CreateFileDocument = gql` mutation CreateFile($file: Upload!) { createFile(file: $file) { id - name - fullPath + path size - type createdAt } } @@ -9970,10 +9966,8 @@ export const DeleteFileDocument = gql` mutation DeleteFile($fileId: UUID!) { deleteFile(fileId: $fileId) { id - name - fullPath + path size - type createdAt } } diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 428669f027..0ddca5f160 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -1480,11 +1480,9 @@ export type FieldsConfiguration = { export type File = { __typename?: 'File'; createdAt: Scalars['DateTime']; - fullPath: Scalars['String']; id: Scalars['UUID']; - name: Scalars['String']; + path: Scalars['String']; size: Scalars['Float']; - type: Scalars['String']; }; export enum FileFolder { diff --git a/packages/twenty-front/src/modules/advanced-text-editor/hooks/useUploadWorkflowFile.ts b/packages/twenty-front/src/modules/advanced-text-editor/hooks/useUploadWorkflowFile.ts index 6315288087..fe20007f74 100644 --- a/packages/twenty-front/src/modules/advanced-text-editor/hooks/useUploadWorkflowFile.ts +++ b/packages/twenty-front/src/modules/advanced-text-editor/hooks/useUploadWorkflowFile.ts @@ -3,7 +3,10 @@ import { formatFileSize } from '@/file/utils/formatFileSize'; import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { t } from '@lingui/core/macro'; -import { isDefined } from 'twenty-shared/utils'; +import { + extractFolderPathFilenameAndTypeOrThrow, + isDefined, +} from 'twenty-shared/utils'; import { useCreateFileMutation } from '~/generated-metadata/graphql'; import { logError } from '~/utils/logError'; @@ -43,11 +46,15 @@ export const useUploadWorkflowFile = () => { throw new Error('File upload failed'); } + const { type } = extractFolderPathFilenameAndTypeOrThrow( + uploadedFile.path, + ); + const workflowFile: WorkflowFile = { id: uploadedFile.id, - name: uploadedFile.name, + name: file.name, size: uploadedFile.size, - type: uploadedFile.type, + type: type, createdAt: uploadedFile.createdAt, }; diff --git a/packages/twenty-front/src/modules/file/graphql/mutations/createFile.ts b/packages/twenty-front/src/modules/file/graphql/mutations/createFile.ts index 8fcfb17c4c..97c13f0914 100644 --- a/packages/twenty-front/src/modules/file/graphql/mutations/createFile.ts +++ b/packages/twenty-front/src/modules/file/graphql/mutations/createFile.ts @@ -4,10 +4,8 @@ export const CREATE_FILE = gql` mutation CreateFile($file: Upload!) { createFile(file: $file) { id - name - fullPath + path size - type createdAt } } diff --git a/packages/twenty-front/src/modules/file/graphql/mutations/deleteFile.ts b/packages/twenty-front/src/modules/file/graphql/mutations/deleteFile.ts index 3b15a2ccad..72ca1e5e02 100644 --- a/packages/twenty-front/src/modules/file/graphql/mutations/deleteFile.ts +++ b/packages/twenty-front/src/modules/file/graphql/mutations/deleteFile.ts @@ -4,10 +4,8 @@ export const DELETE_FILE = gql` mutation DeleteFile($fileId: UUID!) { deleteFile(fileId: $fileId) { id - name - fullPath + path size - type createdAt } } diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1768572831179-updateFileTable.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1768572831179-updateFileTable.ts new file mode 100644 index 0000000000..3479786ef1 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1768572831179-updateFileTable.ts @@ -0,0 +1,57 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class UpdateFileTable1768572831179 implements MigrationInterface { + name = 'UpdateFileTable1768572831179'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "core"."file" DROP COLUMN "name"`); + await queryRunner.query(`ALTER TABLE "core"."file" DROP COLUMN "fullPath"`); + await queryRunner.query(`ALTER TABLE "core"."file" DROP COLUMN "type"`); + await queryRunner.query( + `ALTER TABLE "core"."file" ADD "applicationId" uuid`, + ); + await queryRunner.query( + `ALTER TABLE "core"."file" ADD "path" character varying NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."file" ADD "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "core"."file" ADD "deletedAt" TIMESTAMP WITH TIME ZONE`, + ); + await queryRunner.query( + `ALTER TABLE "core"."file" ADD "isStaticAsset" boolean NOT NULL DEFAULT false`, + ); + await queryRunner.query( + `ALTER TABLE "core"."file" ADD CONSTRAINT "FK_413aaaf293284c3c0266d0bab3a" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE RESTRICT ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."file" DROP CONSTRAINT "FK_413aaaf293284c3c0266d0bab3a"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."file" DROP COLUMN "isStaticAsset"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."file" DROP COLUMN "deletedAt"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."file" DROP COLUMN "updatedAt"`, + ); + await queryRunner.query(`ALTER TABLE "core"."file" DROP COLUMN "path"`); + await queryRunner.query( + `ALTER TABLE "core"."file" DROP COLUMN "applicationId"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."file" ADD "type" character varying NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."file" ADD "fullPath" character varying NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."file" ADD "name" character varying NOT NULL`, + ); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/file/dtos/file.dto.ts b/packages/twenty-server/src/engine/core-modules/file/dtos/file.dto.ts index 39d79dcc7b..fee33833f7 100644 --- a/packages/twenty-server/src/engine/core-modules/file/dtos/file.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/file/dtos/file.dto.ts @@ -8,17 +8,11 @@ export class FileDTO { id: string; @Field() - name: string; - - @Field() - fullPath: string; + path: string; @Field() size: number; - @Field() - type: string; - @Field(() => Date, { nullable: false }) createdAt: Date; } diff --git a/packages/twenty-server/src/engine/core-modules/file/entities/file.entity.ts b/packages/twenty-server/src/engine/core-modules/file/entities/file.entity.ts index 56447b85be..000ad6766f 100644 --- a/packages/twenty-server/src/engine/core-modules/file/entities/file.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/file/entities/file.entity.ts @@ -3,11 +3,17 @@ import { ObjectType } from '@nestjs/graphql'; import { Column, CreateDateColumn, + DeleteDateColumn, Entity, Index, + JoinColumn, + ManyToOne, PrimaryGeneratedColumn, + Relation, + UpdateDateColumn, } from 'typeorm'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity'; @Entity('file') @@ -17,18 +23,30 @@ export class FileEntity extends WorkspaceRelatedEntity { @PrimaryGeneratedColumn('uuid') id: string; - @Column({ nullable: false }) - name: string; + @Column({ nullable: true, type: 'uuid' }) + applicationId: string; + + @ManyToOne('ApplicationEntity', { + onDelete: 'RESTRICT', + }) + @JoinColumn({ name: 'applicationId' }) + application: Relation; @Column({ nullable: false }) - fullPath: string; + path: string; @Column({ nullable: false, type: 'bigint' }) size: number; - @Column({ nullable: false }) - type: string; - @CreateDateColumn({ type: 'timestamptz' }) createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @DeleteDateColumn({ type: 'timestamptz' }) + deletedAt: Date | null; + + @Column({ nullable: false, default: false }) + isStaticAsset: boolean; } diff --git a/packages/twenty-server/src/engine/core-modules/file/jobs/file-deletion.job.ts b/packages/twenty-server/src/engine/core-modules/file/jobs/file-deletion.job.ts index e7557d294e..32d7dde21d 100644 --- a/packages/twenty-server/src/engine/core-modules/file/jobs/file-deletion.job.ts +++ b/packages/twenty-server/src/engine/core-modules/file/jobs/file-deletion.job.ts @@ -1,7 +1,7 @@ import { UnrecoverableError } from 'bullmq'; +import { extractFolderPathFilenameAndTypeOrThrow } from 'twenty-shared/utils'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; -import { extractFolderPathAndFilename } from 'src/engine/core-modules/file/utils/extract-folderpath-and-filename.utils'; import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; @@ -19,7 +19,8 @@ export class FileDeletionJob { async handle(data: FileDeletionJobData): Promise { const { workspaceId, fullPath } = data; - const { folderPath, filename } = extractFolderPathAndFilename(fullPath); + const { folderPath, filename } = + extractFolderPathFilenameAndTypeOrThrow(fullPath); if (!filename) { throw new UnrecoverableError( diff --git a/packages/twenty-server/src/engine/core-modules/file/services/file-metadata.service.ts b/packages/twenty-server/src/engine/core-modules/file/services/file-metadata.service.ts index a817144b89..55440162e9 100644 --- a/packages/twenty-server/src/engine/core-modules/file/services/file-metadata.service.ts +++ b/packages/twenty-server/src/engine/core-modules/file/services/file-metadata.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { extractFolderPathFilenameAndTypeOrThrow } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface'; @@ -8,7 +9,6 @@ import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder. import { type FileDTO } from 'src/engine/core-modules/file/dtos/file.dto'; import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service'; -import { extractFolderPathAndFilename } from 'src/engine/core-modules/file/utils/extract-folderpath-and-filename.utils'; import { FileService } from './file.service'; @@ -45,10 +45,8 @@ export class FileMetadataService { } const createdFile = this.fileRepository.create({ - name: filename, - fullPath: files[0].path, + path: files[0].path, size: file.length, - type: mimeType, workspaceId, }); @@ -69,12 +67,12 @@ export class FileMetadataService { return null; } - const { folderPath, filename } = extractFolderPathAndFilename( - file.fullPath, + const { folderPath, filename } = extractFolderPathFilenameAndTypeOrThrow( + file.path, ); try { - if (file.fullPath) { + if (file.path) { await this.fileService.deleteFile({ folderPath, filename, diff --git a/packages/twenty-server/src/engine/core-modules/file/services/file.service.ts b/packages/twenty-server/src/engine/core-modules/file/services/file.service.ts index 0352196917..37eb40cc3a 100644 --- a/packages/twenty-server/src/engine/core-modules/file/services/file.service.ts +++ b/packages/twenty-server/src/engine/core-modules/file/services/file.service.ts @@ -4,7 +4,10 @@ import { basename, dirname, extname } from 'path'; import { type Readable } from 'stream'; import { isNonEmptyString } from '@sniptt/guards'; -import { buildSignedPath } from 'twenty-shared/utils'; +import { + buildSignedPath, + extractFolderPathFilenameAndTypeOrThrow, +} from 'twenty-shared/utils'; import { v4 as uuidV4 } from 'uuid'; import { @@ -12,7 +15,6 @@ import { JwtTokenTypeEnum, } from 'src/engine/core-modules/auth/types/auth-context.type'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; -import { extractFolderPathAndFilename } from 'src/engine/core-modules/file/utils/extract-folderpath-and-filename.utils'; import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; @@ -45,7 +47,7 @@ export class FileService { return buildSignedPath({ path: url, token: this.encodeFileToken({ - filename: extractFolderPathAndFilename(url).filename, + filename: extractFolderPathFilenameAndTypeOrThrow(url).filename, workspaceId, }), }); diff --git a/packages/twenty-server/src/engine/core-modules/file/utils/extract-folderpath-and-filename.utils.ts b/packages/twenty-server/src/engine/core-modules/file/utils/extract-folderpath-and-filename.utils.ts deleted file mode 100644 index 086c40eb5a..0000000000 --- a/packages/twenty-server/src/engine/core-modules/file/utils/extract-folderpath-and-filename.utils.ts +++ /dev/null @@ -1,13 +0,0 @@ -export function extractFolderPathAndFilename(fullPath: string): { - folderPath: string; - filename: string; -} { - if (!fullPath || typeof fullPath !== 'string') { - throw new Error('Invalid fullPath provided'); - } - const parts = fullPath.split('/'); - const filename = parts.pop() || ''; - const folderPath = parts.join('/'); - - return { folderPath, filename }; -} diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.ts index 5d8ab63696..4fe6c09e77 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.ts @@ -4,13 +4,16 @@ import { InjectRepository } from '@nestjs/typeorm'; import { render, toPlainText } from '@react-email/render'; import DOMPurify from 'dompurify'; import { reactMarkupFromJSON } from 'twenty-emails'; -import { isDefined, isValidUuid } from 'twenty-shared/utils'; +import { + extractFolderPathFilenameAndTypeOrThrow, + isDefined, + isValidUuid, +} from 'twenty-shared/utils'; import { In, type Repository } from 'typeorm'; import { z } from 'zod'; import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; -import { extractFolderPathAndFilename } from 'src/engine/core-modules/file/utils/extract-folderpath-and-filename.utils'; import { SendEmailToolException, SendEmailToolExceptionCode, @@ -154,8 +157,8 @@ export class SendEmailTool implements Tool { for (const fileMetadata of files) { const fileEntity = fileEntityMap.get(fileMetadata.id)!; - const { folderPath, filename } = extractFolderPathAndFilename( - fileEntity.fullPath, + const { folderPath, filename } = extractFolderPathFilenameAndTypeOrThrow( + fileEntity.path, ); const stream = await this.fileService.getFileStream( diff --git a/packages/twenty-shared/src/utils/files/__tests__/extractFolderPathFilenameAndTypeOrThrow.util.test.ts b/packages/twenty-shared/src/utils/files/__tests__/extractFolderPathFilenameAndTypeOrThrow.util.test.ts new file mode 100644 index 0000000000..95f2144cc2 --- /dev/null +++ b/packages/twenty-shared/src/utils/files/__tests__/extractFolderPathFilenameAndTypeOrThrow.util.test.ts @@ -0,0 +1,87 @@ +import { extractFolderPathFilenameAndTypeOrThrow } from '@/utils/files/extractFolderPathFilenameAndTypeOrThrow.util'; + +describe('extractFolderPathFilenameAndType', () => { + it('should extract folder path, filename, and type from a basic path', () => { + const result = extractFolderPathFilenameAndTypeOrThrow( + 'folder/subfolder/file.txt', + ); + + expect(result).toEqual({ + folderPath: 'folder/subfolder', + filename: 'file.txt', + type: 'txt', + }); + }); + + it('should handle multiple dots in filename', () => { + const result = extractFolderPathFilenameAndTypeOrThrow( + 'path/to/file.config.json', + ); + + expect(result).toEqual({ + folderPath: 'path/to', + filename: 'file.config.json', + type: 'json', + }); + }); + + it('should handle filename without extension', () => { + const result = extractFolderPathFilenameAndTypeOrThrow('folder/Makefile'); + + expect(result).toEqual({ + folderPath: 'folder', + filename: 'Makefile', + type: '', + }); + }); + + it('should handle file at root level', () => { + const result = extractFolderPathFilenameAndTypeOrThrow('document.pdf'); + + expect(result).toEqual({ + folderPath: '', + filename: 'document.pdf', + type: 'pdf', + }); + }); + + it('should handle deeply nested paths', () => { + const result = extractFolderPathFilenameAndTypeOrThrow( + 'a/b/c/d/e/image.png', + ); + + expect(result).toEqual({ + folderPath: 'a/b/c/d/e', + filename: 'image.png', + type: 'png', + }); + }); + + it('should handle hidden files', () => { + const result = extractFolderPathFilenameAndTypeOrThrow('folder/.gitignore'); + + expect(result).toEqual({ + folderPath: 'folder', + filename: '.gitignore', + type: 'gitignore', + }); + }); + + it('should throw error for empty string', () => { + expect(() => extractFolderPathFilenameAndTypeOrThrow('')).toThrow( + 'Invalid fullPath provided', + ); + }); + + it('should throw error for null value', () => { + expect(() => + extractFolderPathFilenameAndTypeOrThrow(null as unknown as string), + ).toThrow('Invalid fullPath provided'); + }); + + it('should throw error for undefined value', () => { + expect(() => + extractFolderPathFilenameAndTypeOrThrow(undefined as unknown as string), + ).toThrow('Invalid fullPath provided'); + }); +}); diff --git a/packages/twenty-shared/src/utils/files/extractFolderPathFilenameAndTypeOrThrow.util.ts b/packages/twenty-shared/src/utils/files/extractFolderPathFilenameAndTypeOrThrow.util.ts new file mode 100644 index 0000000000..bbb68203db --- /dev/null +++ b/packages/twenty-shared/src/utils/files/extractFolderPathFilenameAndTypeOrThrow.util.ts @@ -0,0 +1,21 @@ +import { isNonEmptyString } from '@sniptt/guards'; + +export const extractFolderPathFilenameAndTypeOrThrow = ( + fullPath: string, +): { + folderPath: string; + filename: string; + type: string; +} => { + if (!isNonEmptyString(fullPath)) { + throw new Error('Invalid fullPath provided'); + } + const parts = fullPath.split('/'); + const filename = parts.pop() || ''; + const folderPath = parts.join('/'); + + const lastDotIndex = filename.lastIndexOf('.'); + const extension = lastDotIndex !== -1 ? filename.slice(lastDotIndex + 1) : ''; + + return { folderPath, filename, type: extension }; +}; diff --git a/packages/twenty-shared/src/utils/index.ts b/packages/twenty-shared/src/utils/index.ts index 0a399481e8..3c0935c768 100644 --- a/packages/twenty-shared/src/utils/index.ts +++ b/packages/twenty-shared/src/utils/index.ts @@ -42,6 +42,7 @@ export { isFieldMetadataDateKind } from './fieldMetadata/isFieldMetadataDateKind export { isFieldMetadataNumericKind } from './fieldMetadata/isFieldMetadataNumericKind'; export { isFieldMetadataSelectKind } from './fieldMetadata/isFieldMetadataSelectKind'; export { isFieldMetadataTextKind } from './fieldMetadata/isFieldMetadataTextKind'; +export { extractFolderPathFilenameAndTypeOrThrow } from './files/extractFolderPathFilenameAndTypeOrThrow.util'; export { checkIfShouldComputeEmptinessFilter } from './filter/checkIfShouldComputeEmptinessFilter'; export { checkIfShouldSkipFiltering } from './filter/checkIfShouldSkipFiltering'; export { computeGqlOperationFilterForEmails } from './filter/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForEmails';