feat: configure standard views and migrate attachment seeds to FILES field (#17958)
## Summary - Add default visible view fields for `timelineActivity`, `attachment`, `noteTarget`, `taskTarget`, and `workspaceMember` objects so they display useful columns out of the box - Standardize morph relation field labels to "Target" with `IconArrowUpRight` for consistency across all pivot/junction tables - Mark deprecated fields (`fullPath`, `fileCategory`, `linkedRecordCachedName`, `linkedRecordId`, `linkedObjectMetadataId`) as `isSystem` to hide them from the UI column picker - Fix morph field deduplication logic (`pickMorphGroupSurvivor`) to prefer active, non-system fields over auto-generated system fields from custom objects - Migrate attachment seeds from legacy `fullPath`/`fileCategory` to the new `FILES` field type, creating proper `FileEntity` records in `core.file` via `fileStorageService.writeFile()` - Restore `customDomain` in the user query fragment <img width="825" height="754" alt="Screenshot 2026-02-15 at 15 44 27" src="https://github.com/user-attachments/assets/9596a3dd-8d3a-43c0-925a-0adef9ee68a8" /> <img width="736" height="731" alt="Screenshot 2026-02-15 at 15 44 13" src="https://github.com/user-attachments/assets/cd1a66c5-731d-43e6-bbc3-703cbeda1652" /> <img width="722" height="757" alt="Screenshot 2026-02-15 at 15 44 03" src="https://github.com/user-attachments/assets/b5210546-6a40-4940-8e4f-874818a614fb" /> <img width="907" height="757" alt="Screenshot 2026-02-15 at 15 43 52" src="https://github.com/user-attachments/assets/ead5b9a8-1989-4d68-9640-583da6233711" /> <img width="1002" height="731" alt="Screenshot 2026-02-15 at 15 43 38" src="https://github.com/user-attachments/assets/38accb8c-f5d5-4bfc-b245-06389849810b" /> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches migration/upgrade commands that write to core metadata tables and adjust field/view definitions, plus changes dev seeding to create `core.file` records; mistakes could affect UI visibility or seed integrity across workspaces. > > **Overview** > Adds a new `upgrade:1-18:backfill-standard-views-and-field-metadata` command that, per workspace, marks specific fields as `isSystem`, normalizes morph-relation field `label`/`icon` to `Target`/`IconArrowUpRight`, and backfills missing standard `view`/`viewField` rows for `attachment`, `noteTarget`, `taskTarget`, `timelineActivity`, and `workspaceMember`, followed by cache invalidation + metadata version bump. > > Refactors morph-relation deduplication to pick a single survivor per `morphId` using a new `pickMorphGroupSurvivor` rule (prefer active + non-system, then smallest id), with new unit tests. > > Updates standard metadata generators and snapshots to reflect the new system flags and default view fields, and rewrites attachment dev seeding to populate the new `file` (FILES field) JSON and create corresponding `core.file` entries via `FileStorageService.writeFile` with workspace-scoped file IDs. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b1939bbf6f8cce294f9b4cdec06b19778daa205e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
This commit is contained in:
+114
-168
@@ -10,8 +10,7 @@ import { WORKSPACE_MEMBER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev
|
||||
type AttachmentDataSeed = {
|
||||
id: string;
|
||||
name: string;
|
||||
fullPath: string;
|
||||
fileCategory: string;
|
||||
file: string;
|
||||
createdBySource: string;
|
||||
createdByWorkspaceMemberId: string;
|
||||
createdByName: string;
|
||||
@@ -28,8 +27,7 @@ type AttachmentDataSeed = {
|
||||
export const ATTACHMENT_DATA_SEED_COLUMNS: (keyof AttachmentDataSeed)[] = [
|
||||
'id',
|
||||
'name',
|
||||
'fullPath',
|
||||
'fileCategory',
|
||||
'file',
|
||||
'createdBySource',
|
||||
'createdByWorkspaceMemberId',
|
||||
'createdByName',
|
||||
@@ -58,203 +56,153 @@ const GENERATE_ATTACHMENT_IDS = (): Record<string, string> => {
|
||||
|
||||
export const ATTACHMENT_DATA_SEED_IDS = GENERATE_ATTACHMENT_IDS();
|
||||
|
||||
// Pool of 5 reusable file paths for attachments
|
||||
const FILE_TEMPLATE_PATHS = [
|
||||
'attachment/sample-contract.pdf',
|
||||
'attachment/budget-2024.xlsx',
|
||||
'attachment/presentation.pptx',
|
||||
'attachment/screenshot.png',
|
||||
'attachment/archive.zip',
|
||||
// FileIds must be unique per workspace since core.file is a shared table.
|
||||
// We use the first 12 hex chars of the workspaceId as a namespace suffix.
|
||||
const deriveFileId = (attachmentIndex: number, workspaceId: string): string => {
|
||||
const workspaceHex = workspaceId.replace(/-/g, '').slice(0, 12);
|
||||
const hexIndex = attachmentIndex.toString(16).padStart(4, '0');
|
||||
|
||||
return `f11e0000-${hexIndex}-4a7c-8001-${workspaceHex}`;
|
||||
};
|
||||
|
||||
export const ATTACHMENT_SAMPLE_FILES = [
|
||||
{
|
||||
filename: 'sample-contract.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
extension: 'pdf',
|
||||
},
|
||||
{
|
||||
filename: 'budget-2024.xlsx',
|
||||
mimeType:
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
extension: 'xlsx',
|
||||
},
|
||||
{
|
||||
filename: 'presentation.pptx',
|
||||
mimeType:
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
extension: 'pptx',
|
||||
},
|
||||
{
|
||||
filename: 'screenshot.png',
|
||||
mimeType: 'image/png',
|
||||
extension: 'png',
|
||||
},
|
||||
{
|
||||
filename: 'archive.zip',
|
||||
mimeType: 'application/zip',
|
||||
extension: 'zip',
|
||||
},
|
||||
];
|
||||
|
||||
// Additional name variations for more realistic variety
|
||||
const FILE_NAME_VARIATIONS = [
|
||||
// Documents
|
||||
{
|
||||
name: 'Service Agreement.pdf',
|
||||
fileCategory: 'TEXT_DOCUMENT',
|
||||
pathIndex: 0,
|
||||
},
|
||||
{
|
||||
name: 'NDA Document.pdf',
|
||||
fileCategory: 'TEXT_DOCUMENT',
|
||||
pathIndex: 0,
|
||||
},
|
||||
{
|
||||
name: 'Project Proposal.pdf',
|
||||
fileCategory: 'TEXT_DOCUMENT',
|
||||
pathIndex: 0,
|
||||
},
|
||||
{
|
||||
name: 'Invoice Q1 2024.pdf',
|
||||
fileCategory: 'TEXT_DOCUMENT',
|
||||
pathIndex: 0,
|
||||
},
|
||||
{
|
||||
name: 'Meeting Notes.pdf',
|
||||
fileCategory: 'TEXT_DOCUMENT',
|
||||
pathIndex: 0,
|
||||
},
|
||||
{
|
||||
name: 'Report Final.pdf',
|
||||
fileCategory: 'TEXT_DOCUMENT',
|
||||
pathIndex: 0,
|
||||
},
|
||||
{
|
||||
name: 'Contract Signed.pdf',
|
||||
fileCategory: 'TEXT_DOCUMENT',
|
||||
pathIndex: 0,
|
||||
},
|
||||
{ name: 'Service Agreement.pdf', sampleFileIndex: 0 },
|
||||
{ name: 'NDA Document.pdf', sampleFileIndex: 0 },
|
||||
{ name: 'Project Proposal.pdf', sampleFileIndex: 0 },
|
||||
{ name: 'Invoice Q1 2024.pdf', sampleFileIndex: 0 },
|
||||
{ name: 'Meeting Notes.pdf', sampleFileIndex: 0 },
|
||||
{ name: 'Report Final.pdf', sampleFileIndex: 0 },
|
||||
{ name: 'Contract Signed.pdf', sampleFileIndex: 0 },
|
||||
// Spreadsheets
|
||||
{
|
||||
name: 'Financial Forecast.xlsx',
|
||||
fileCategory: 'SPREADSHEET',
|
||||
pathIndex: 1,
|
||||
},
|
||||
{
|
||||
name: 'Sales Report Q4.xlsx',
|
||||
fileCategory: 'SPREADSHEET',
|
||||
pathIndex: 1,
|
||||
},
|
||||
{
|
||||
name: 'Team Roster.xlsx',
|
||||
fileCategory: 'SPREADSHEET',
|
||||
pathIndex: 1,
|
||||
},
|
||||
{
|
||||
name: 'Expense Report.xlsx',
|
||||
fileCategory: 'SPREADSHEET',
|
||||
pathIndex: 1,
|
||||
},
|
||||
{
|
||||
name: 'Inventory List.xlsx',
|
||||
fileCategory: 'SPREADSHEET',
|
||||
pathIndex: 1,
|
||||
},
|
||||
{
|
||||
name: 'Data Export.csv',
|
||||
fileCategory: 'SPREADSHEET',
|
||||
pathIndex: 1,
|
||||
},
|
||||
{ name: 'Financial Forecast.xlsx', sampleFileIndex: 1 },
|
||||
{ name: 'Sales Report Q4.xlsx', sampleFileIndex: 1 },
|
||||
{ name: 'Team Roster.xlsx', sampleFileIndex: 1 },
|
||||
{ name: 'Expense Report.xlsx', sampleFileIndex: 1 },
|
||||
{ name: 'Inventory List.xlsx', sampleFileIndex: 1 },
|
||||
{ name: 'Data Export.csv', sampleFileIndex: 1 },
|
||||
// Presentations
|
||||
{
|
||||
name: 'Pitch Deck.pptx',
|
||||
fileCategory: 'PRESENTATION',
|
||||
pathIndex: 2,
|
||||
},
|
||||
{
|
||||
name: 'Q4 Results.pptx',
|
||||
fileCategory: 'PRESENTATION',
|
||||
pathIndex: 2,
|
||||
},
|
||||
{
|
||||
name: 'Roadmap 2024.pptx',
|
||||
fileCategory: 'PRESENTATION',
|
||||
pathIndex: 2,
|
||||
},
|
||||
{
|
||||
name: 'Company Overview.pptx',
|
||||
fileCategory: 'PRESENTATION',
|
||||
pathIndex: 2,
|
||||
},
|
||||
{
|
||||
name: 'Training Materials.pptx',
|
||||
fileCategory: 'PRESENTATION',
|
||||
pathIndex: 2,
|
||||
},
|
||||
{ name: 'Pitch Deck.pptx', sampleFileIndex: 2 },
|
||||
{ name: 'Q4 Results.pptx', sampleFileIndex: 2 },
|
||||
{ name: 'Roadmap 2024.pptx', sampleFileIndex: 2 },
|
||||
{ name: 'Company Overview.pptx', sampleFileIndex: 2 },
|
||||
{ name: 'Training Materials.pptx', sampleFileIndex: 2 },
|
||||
// Images
|
||||
{
|
||||
name: 'Company Logo.png',
|
||||
fileCategory: 'IMAGE',
|
||||
pathIndex: 3,
|
||||
},
|
||||
{
|
||||
name: 'Product Photo.jpg',
|
||||
fileCategory: 'IMAGE',
|
||||
pathIndex: 3,
|
||||
},
|
||||
{ name: 'Diagram.png', fileCategory: 'IMAGE', pathIndex: 3 },
|
||||
{ name: 'Wireframe.png', fileCategory: 'IMAGE', pathIndex: 3 },
|
||||
{
|
||||
name: 'Mockup Design.png',
|
||||
fileCategory: 'IMAGE',
|
||||
pathIndex: 3,
|
||||
},
|
||||
{ name: 'Headshot.jpg', fileCategory: 'IMAGE', pathIndex: 3 },
|
||||
{ name: 'Company Logo.png', sampleFileIndex: 3 },
|
||||
{ name: 'Product Photo.jpg', sampleFileIndex: 3 },
|
||||
{ name: 'Diagram.png', sampleFileIndex: 3 },
|
||||
{ name: 'Wireframe.png', sampleFileIndex: 3 },
|
||||
{ name: 'Mockup Design.png', sampleFileIndex: 3 },
|
||||
{ name: 'Headshot.jpg', sampleFileIndex: 3 },
|
||||
// Archives
|
||||
{
|
||||
name: 'Project Files.zip',
|
||||
fileCategory: 'ARCHIVE',
|
||||
pathIndex: 4,
|
||||
},
|
||||
{
|
||||
name: 'Backup Data.zip',
|
||||
fileCategory: 'ARCHIVE',
|
||||
pathIndex: 4,
|
||||
},
|
||||
{
|
||||
name: 'Source Code.zip',
|
||||
fileCategory: 'ARCHIVE',
|
||||
pathIndex: 4,
|
||||
},
|
||||
{ name: 'Project Files.zip', sampleFileIndex: 4 },
|
||||
{ name: 'Backup Data.zip', sampleFileIndex: 4 },
|
||||
{ name: 'Source Code.zip', sampleFileIndex: 4 },
|
||||
];
|
||||
|
||||
const GENERATE_ATTACHMENT_SEEDS = (): AttachmentDataSeed[] => {
|
||||
const ATTACHMENT_SEEDS: AttachmentDataSeed[] = [];
|
||||
export type AttachmentFileSeedMetadata = {
|
||||
fileId: string;
|
||||
label: string;
|
||||
sampleFileIndex: number;
|
||||
mimeType: string;
|
||||
extension: string;
|
||||
};
|
||||
|
||||
// Get available entity IDs
|
||||
const PERSON_IDS = Object.values(PERSON_DATA_SEED_IDS).slice(0, 120); // Use first 120 persons
|
||||
const COMPANY_IDS = Object.values(COMPANY_DATA_SEED_IDS).slice(0, 120); // Use first 120 companies
|
||||
const NOTE_IDS = Object.values(NOTE_DATA_SEED_IDS).slice(0, 80); // Use first 80 notes
|
||||
const TASK_IDS = Object.values(TASK_DATA_SEED_IDS).slice(0, 60); // Use first 60 tasks
|
||||
const OPPORTUNITY_IDS = Object.values(OPPORTUNITY_DATA_SEED_IDS).slice(0, 20); // Use first 20 opportunities
|
||||
export const generateAttachmentSeedsForWorkspace = (
|
||||
workspaceId: string,
|
||||
): {
|
||||
seeds: AttachmentDataSeed[];
|
||||
fileSeedMetadata: AttachmentFileSeedMetadata[];
|
||||
} => {
|
||||
const seeds: AttachmentDataSeed[] = [];
|
||||
const fileSeedMetadata: AttachmentFileSeedMetadata[] = [];
|
||||
|
||||
const PERSON_IDS = Object.values(PERSON_DATA_SEED_IDS).slice(0, 120);
|
||||
const COMPANY_IDS = Object.values(COMPANY_DATA_SEED_IDS).slice(0, 120);
|
||||
const NOTE_IDS = Object.values(NOTE_DATA_SEED_IDS).slice(0, 80);
|
||||
const TASK_IDS = Object.values(TASK_DATA_SEED_IDS).slice(0, 60);
|
||||
const OPPORTUNITY_IDS = Object.values(OPPORTUNITY_DATA_SEED_IDS).slice(0, 20);
|
||||
|
||||
let entityIndex = 0;
|
||||
|
||||
for (let INDEX = 1; INDEX <= 400; INDEX++) {
|
||||
// Cycle through file name variations
|
||||
const NAME_VARIATION_INDEX = INDEX % FILE_NAME_VARIATIONS.length;
|
||||
const NAME_VARIATION = FILE_NAME_VARIATIONS[NAME_VARIATION_INDEX];
|
||||
const FILE_PATH = FILE_TEMPLATE_PATHS[NAME_VARIATION.pathIndex];
|
||||
for (let index = 1; index <= 400; index++) {
|
||||
const nameVariationIndex = index % FILE_NAME_VARIATIONS.length;
|
||||
const nameVariation = FILE_NAME_VARIATIONS[nameVariationIndex];
|
||||
const sampleFile = ATTACHMENT_SAMPLE_FILES[nameVariation.sampleFileIndex];
|
||||
|
||||
const attachmentId = ATTACHMENT_DATA_SEED_IDS[`ID_${index}`];
|
||||
const fileId = deriveFileId(index, workspaceId);
|
||||
|
||||
// Determine which entity this attachment belongs to
|
||||
// Distribution: ~30% person, ~30% company, ~20% note, ~15% task, ~5% opportunity
|
||||
let targetPersonId: string | null = null;
|
||||
let targetCompanyId: string | null = null;
|
||||
let targetNoteId: string | null = null;
|
||||
let targetTaskId: string | null = null;
|
||||
let targetOpportunityId: string | null = null;
|
||||
|
||||
const DISTRIBUTION_VALUE = INDEX % 100;
|
||||
const distributionValue = index % 100;
|
||||
|
||||
if (DISTRIBUTION_VALUE < 30) {
|
||||
// 30% Person attachments
|
||||
if (distributionValue < 30) {
|
||||
targetPersonId = PERSON_IDS[entityIndex % PERSON_IDS.length];
|
||||
entityIndex++;
|
||||
} else if (DISTRIBUTION_VALUE < 60) {
|
||||
// 30% Company attachments
|
||||
} else if (distributionValue < 60) {
|
||||
targetCompanyId = COMPANY_IDS[entityIndex % COMPANY_IDS.length];
|
||||
entityIndex++;
|
||||
} else if (DISTRIBUTION_VALUE < 80) {
|
||||
// 20% Note attachments
|
||||
} else if (distributionValue < 80) {
|
||||
targetNoteId = NOTE_IDS[entityIndex % NOTE_IDS.length];
|
||||
entityIndex++;
|
||||
} else if (DISTRIBUTION_VALUE < 95) {
|
||||
// 15% Task attachments
|
||||
} else if (distributionValue < 95) {
|
||||
targetTaskId = TASK_IDS[entityIndex % TASK_IDS.length];
|
||||
entityIndex++;
|
||||
} else {
|
||||
// 5% Opportunity attachments
|
||||
targetOpportunityId =
|
||||
OPPORTUNITY_IDS[entityIndex % OPPORTUNITY_IDS.length];
|
||||
entityIndex++;
|
||||
}
|
||||
|
||||
ATTACHMENT_SEEDS.push({
|
||||
id: ATTACHMENT_DATA_SEED_IDS[`ID_${INDEX}`],
|
||||
name: NAME_VARIATION.name,
|
||||
fullPath: FILE_PATH,
|
||||
fileCategory: NAME_VARIATION.fileCategory,
|
||||
fileSeedMetadata.push({
|
||||
fileId,
|
||||
label: nameVariation.name,
|
||||
sampleFileIndex: nameVariation.sampleFileIndex,
|
||||
mimeType: sampleFile.mimeType,
|
||||
extension: sampleFile.extension,
|
||||
});
|
||||
|
||||
seeds.push({
|
||||
id: attachmentId,
|
||||
name: nameVariation.name,
|
||||
file: JSON.stringify([
|
||||
{ fileId, label: nameVariation.name, extension: sampleFile.extension },
|
||||
]),
|
||||
createdBySource: FieldActorSource.MANUAL,
|
||||
createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
|
||||
createdByName: 'Tim A',
|
||||
@@ -269,7 +217,5 @@ const GENERATE_ATTACHMENT_SEEDS = (): AttachmentDataSeed[] => {
|
||||
});
|
||||
}
|
||||
|
||||
return ATTACHMENT_SEEDS;
|
||||
return { seeds, fileSeedMetadata };
|
||||
};
|
||||
|
||||
export const ATTACHMENT_DATA_SEEDS = GENERATE_ATTACHMENT_SEEDS();
|
||||
|
||||
+58
-35
@@ -4,6 +4,8 @@ import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
@@ -15,7 +17,9 @@ import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manage
|
||||
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
|
||||
import {
|
||||
ATTACHMENT_DATA_SEED_COLUMNS,
|
||||
ATTACHMENT_DATA_SEEDS,
|
||||
ATTACHMENT_SAMPLE_FILES,
|
||||
type AttachmentFileSeedMetadata,
|
||||
generateAttachmentSeedsForWorkspace,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/data/constants/attachment-data-seeds.constant';
|
||||
import {
|
||||
CALENDAR_CHANNEL_DATA_SEED_COLUMNS,
|
||||
@@ -115,6 +119,7 @@ import {
|
||||
} from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
|
||||
import { TimelineActivitySeederService } from 'src/engine/workspace-manager/dev-seeder/data/services/timeline-activity-seeder.service';
|
||||
import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows';
|
||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
|
||||
|
||||
type RecordSeedConfig = {
|
||||
tableName: string;
|
||||
@@ -125,6 +130,7 @@ type RecordSeedConfig = {
|
||||
// Organize seeds into dependency batches for parallel insertion
|
||||
const getRecordSeedsBatches = (
|
||||
workspaceId: string,
|
||||
attachmentSeeds: RecordSeedConfig['recordSeeds'],
|
||||
_featureFlags?: Record<FeatureFlagKey, boolean>,
|
||||
): RecordSeedConfig[][] => {
|
||||
// Batch 1: No dependencies
|
||||
@@ -273,7 +279,7 @@ const getRecordSeedsBatches = (
|
||||
{
|
||||
tableName: 'attachment',
|
||||
pgColumns: ATTACHMENT_DATA_SEED_COLUMNS,
|
||||
recordSeeds: ATTACHMENT_DATA_SEEDS,
|
||||
recordSeeds: attachmentSeeds,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -311,12 +317,16 @@ export class DevSeederDataService {
|
||||
},
|
||||
);
|
||||
|
||||
const { seeds: attachmentSeeds, fileSeedMetadata: attachmentFileMeta } =
|
||||
generateAttachmentSeedsForWorkspace(workspaceId);
|
||||
|
||||
await this.coreDataSource.transaction(
|
||||
async (entityManager: WorkspaceEntityManager) => {
|
||||
await this.seedRecordsInBatches({
|
||||
entityManager,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
attachmentSeeds,
|
||||
featureFlags,
|
||||
objectMetadataItems,
|
||||
});
|
||||
@@ -327,7 +337,11 @@ export class DevSeederDataService {
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
await this.seedAttachmentFiles(workspaceId);
|
||||
await this.seedAttachmentFiles(
|
||||
workspaceId,
|
||||
entityManager,
|
||||
attachmentFileMeta,
|
||||
);
|
||||
|
||||
await prefillWorkflows(
|
||||
entityManager,
|
||||
@@ -343,16 +357,22 @@ export class DevSeederDataService {
|
||||
entityManager,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
attachmentSeeds,
|
||||
featureFlags,
|
||||
objectMetadataItems,
|
||||
}: {
|
||||
entityManager: WorkspaceEntityManager;
|
||||
schemaName: string;
|
||||
workspaceId: string;
|
||||
attachmentSeeds: RecordSeedConfig['recordSeeds'];
|
||||
featureFlags?: Record<FeatureFlagKey, boolean>;
|
||||
objectMetadataItems: FlatObjectMetadata[];
|
||||
}) {
|
||||
const batches = getRecordSeedsBatches(workspaceId, featureFlags);
|
||||
const batches = getRecordSeedsBatches(
|
||||
workspaceId,
|
||||
attachmentSeeds,
|
||||
featureFlags,
|
||||
);
|
||||
|
||||
// Process batches sequentially (respecting dependencies)
|
||||
// but entities within each batch in parallel
|
||||
@@ -406,9 +426,11 @@ export class DevSeederDataService {
|
||||
.execute();
|
||||
}
|
||||
|
||||
private async seedAttachmentFiles(workspaceId: string): Promise<void> {
|
||||
// Files are copied to dist/assets during build via nest-cli.json
|
||||
// The pattern **/dev-seeder/data/sample-files/** preserves the full path
|
||||
private async seedAttachmentFiles(
|
||||
workspaceId: string,
|
||||
entityManager: WorkspaceEntityManager,
|
||||
fileSeedMetadata: AttachmentFileSeedMetadata[],
|
||||
): Promise<void> {
|
||||
const IS_BUILT = __dirname.includes('/dist/');
|
||||
const sampleFilesDir = IS_BUILT
|
||||
? join(
|
||||
@@ -417,37 +439,38 @@ export class DevSeederDataService {
|
||||
)
|
||||
: join(__dirname, '../sample-files');
|
||||
|
||||
const filesToCreate = [
|
||||
'sample-contract.pdf',
|
||||
'budget-2024.xlsx',
|
||||
'presentation.pptx',
|
||||
'screenshot.png',
|
||||
'archive.zip',
|
||||
];
|
||||
// Read each sample file once and cache the buffer
|
||||
const sampleFileBuffers: Buffer[] = [];
|
||||
|
||||
for (const filename of filesToCreate) {
|
||||
const filePath = join(sampleFilesDir, filename);
|
||||
const fileBuffer = await readFile(filePath);
|
||||
for (const sampleFile of ATTACHMENT_SAMPLE_FILES) {
|
||||
const filePath = join(sampleFilesDir, sampleFile.filename);
|
||||
|
||||
await this.fileStorageService.writeFileLegacy({
|
||||
file: fileBuffer,
|
||||
name: filename,
|
||||
folder: `workspace-${workspaceId}/attachment`,
|
||||
mimeType: this.getMimeType(filename),
|
||||
sampleFileBuffers.push(await readFile(filePath));
|
||||
}
|
||||
|
||||
const fieldUniversalIdentifier =
|
||||
STANDARD_OBJECTS.attachment.fields.file.universalIdentifier;
|
||||
const applicationUniversalIdentifier =
|
||||
TWENTY_STANDARD_APPLICATION.universalIdentifier;
|
||||
|
||||
for (const metadata of fileSeedMetadata) {
|
||||
const resourcePath = `${metadata.fileId}.${metadata.extension}`;
|
||||
const sourceFile = sampleFileBuffers[metadata.sampleFileIndex];
|
||||
|
||||
await this.fileStorageService.writeFile({
|
||||
sourceFile,
|
||||
mimeType: metadata.mimeType,
|
||||
fileFolder: FileFolder.FilesField,
|
||||
applicationUniversalIdentifier,
|
||||
workspaceId,
|
||||
resourcePath: `${fieldUniversalIdentifier}/${resourcePath}`,
|
||||
fileId: metadata.fileId,
|
||||
settings: {
|
||||
isTemporaryFile: false,
|
||||
toDelete: false,
|
||||
},
|
||||
queryRunner: entityManager.queryRunner,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private getMimeType(filename: string): string {
|
||||
const ext = filename.split('.').pop()?.toLowerCase();
|
||||
const mimeTypes: Record<string, string> = {
|
||||
pdf: 'application/pdf',
|
||||
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
png: 'image/png',
|
||||
zip: 'application/zip',
|
||||
};
|
||||
|
||||
return mimeTypes[ext || ''] || 'application/octet-stream';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user