Scaffold light twenty app dev container (#18734)

as title
This commit is contained in:
martmull
2026-03-18 20:10:54 +01:00
committed by GitHub
parent 394a3cef15
commit 2f095c8903
44 changed files with 861 additions and 371 deletions
@@ -46,10 +46,12 @@ export class DevSeederPermissionsService {
twentyStandardFlatApplication,
workspaceCustomFlatApplication,
workspaceId,
light = false,
}: {
workspaceId: string;
twentyStandardFlatApplication: FlatApplication;
workspaceCustomFlatApplication: FlatApplication;
light?: boolean;
}) {
const adminRole = await this.roleRepository.findOne({
where: {
@@ -80,35 +82,49 @@ export class DevSeederPermissionsService {
let guestUserWorkspaceId: string | undefined;
if (workspaceId === SEED_APPLE_WORKSPACE_ID) {
adminUserWorkspaceId = USER_WORKSPACE_DATA_SEED_IDS.JANE;
limitedUserWorkspaceId = USER_WORKSPACE_DATA_SEED_IDS.TIM;
memberUserWorkspaceIds = [
USER_WORKSPACE_DATA_SEED_IDS.JONY,
...Object.values(RANDOM_USER_WORKSPACE_IDS),
];
guestUserWorkspaceId = USER_WORKSPACE_DATA_SEED_IDS.PHIL;
if (light) {
// In light mode, Tim is admin (prefilled login user needs full
// access for SDK development). No demo permission roles needed.
adminUserWorkspaceId = USER_WORKSPACE_DATA_SEED_IDS.TIM;
memberUserWorkspaceIds = [
USER_WORKSPACE_DATA_SEED_IDS.JANE,
USER_WORKSPACE_DATA_SEED_IDS.JONY,
USER_WORKSPACE_DATA_SEED_IDS.PHIL,
...Object.values(RANDOM_USER_WORKSPACE_IDS),
];
} else {
adminUserWorkspaceId = USER_WORKSPACE_DATA_SEED_IDS.JANE;
limitedUserWorkspaceId = USER_WORKSPACE_DATA_SEED_IDS.TIM;
guestUserWorkspaceId = USER_WORKSPACE_DATA_SEED_IDS.PHIL;
memberUserWorkspaceIds = [
USER_WORKSPACE_DATA_SEED_IDS.JONY,
...Object.values(RANDOM_USER_WORKSPACE_IDS),
];
const guestRole = await this.roleService.createGuestRole({
workspaceId,
ownerFlatApplication: workspaceCustomFlatApplication,
});
const guestRole = await this.roleService.createGuestRole({
workspaceId,
ownerFlatApplication: workspaceCustomFlatApplication,
});
await this.userRoleService.assignRoleToManyUserWorkspace({
workspaceId,
userWorkspaceIds: [guestUserWorkspaceId],
roleId: guestRole.id,
});
await this.userRoleService.assignRoleToManyUserWorkspace({
workspaceId,
userWorkspaceIds: [guestUserWorkspaceId],
roleId: guestRole.id,
});
const limitedRole = await this.createLimitedRoleForSeedWorkspace({
workspaceId,
ownerFlatApplication: workspaceCustomFlatApplication,
});
// The limited role restricts access to Pet and Rocket objects,
// which are only created in full (non-light) mode
const limitedRole = await this.createLimitedRoleForSeedWorkspace({
workspaceId,
ownerFlatApplication: workspaceCustomFlatApplication,
});
await this.userRoleService.assignRoleToManyUserWorkspace({
workspaceId,
userWorkspaceIds: [limitedUserWorkspaceId],
roleId: limitedRole.id,
});
await this.userRoleService.assignRoleToManyUserWorkspace({
workspaceId,
userWorkspaceIds: [limitedUserWorkspaceId],
roleId: limitedRole.id,
});
}
} else if (workspaceId === SEED_YCOMBINATOR_WORKSPACE_ID) {
adminUserWorkspaceId = USER_WORKSPACE_DATA_SEED_IDS.TIM_ACME;
memberUserWorkspaceIds = [
@@ -300,10 +300,12 @@ export class DevSeederDataService {
schemaName,
workspaceId,
featureFlags,
light = false,
}: {
schemaName: string;
workspaceId: string;
featureFlags?: Record<FeatureFlagKey, boolean>;
light?: boolean;
}) {
const objectMetadataItems =
await this.objectMetadataService.findManyWithinWorkspace(workspaceId);
@@ -328,19 +330,22 @@ export class DevSeederDataService {
attachmentSeeds,
featureFlags,
objectMetadataItems,
light,
});
await this.timelineActivitySeederService.seedTimelineActivities({
entityManager,
schemaName,
workspaceId,
});
if (!light) {
await this.timelineActivitySeederService.seedTimelineActivities({
entityManager,
schemaName,
workspaceId,
});
await this.seedAttachmentFiles(
workspaceId,
entityManager,
attachmentFileMeta,
);
await this.seedAttachmentFiles(
workspaceId,
entityManager,
attachmentFileMeta,
);
}
await prefillWorkflows(
entityManager,
@@ -359,6 +364,7 @@ export class DevSeederDataService {
attachmentSeeds,
featureFlags,
objectMetadataItems,
light = false,
}: {
entityManager: WorkspaceEntityManager;
schemaName: string;
@@ -366,6 +372,7 @@ export class DevSeederDataService {
attachmentSeeds: RecordSeedConfig['recordSeeds'];
featureFlags?: Record<FeatureFlagKey, boolean>;
objectMetadataItems: FlatObjectMetadata[];
light?: boolean;
}) {
const batches = getRecordSeedsBatches(
workspaceId,
@@ -378,6 +385,10 @@ export class DevSeederDataService {
for (const batch of batches) {
await Promise.all(
batch.map(async (recordSeedsConfig) => {
if (light && recordSeedsConfig.tableName.startsWith('_')) {
return;
}
const objectMetadata = objectMetadataItems.find(
(item) =>
computeTableName(item.nameSingular, item.isCustom) ===
@@ -51,6 +51,14 @@ type JunctionConfigSeed = {
label?: string;
};
type WorkspaceSeedConfig = {
objects: { seed: ObjectMetadataSeed; fields?: FieldMetadataSeed[] }[];
fields: { objectName: string; seeds: FieldMetadataSeed[] }[];
morphRelations?: { objectName: string; seeds: MorphRelationSeed[] }[];
junctionFields?: JunctionFieldSeed[];
junctionConfigs?: JunctionConfigSeed[];
};
type FlatMaps = {
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
@@ -65,18 +73,7 @@ export class DevSeederMetadataService {
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
private readonly workspaceConfigs: Record<
string,
{
objects: { seed: ObjectMetadataSeed; fields?: FieldMetadataSeed[] }[];
fields: { objectName: string; seeds: FieldMetadataSeed[] }[];
morphRelations?: { objectName: string; seeds: MorphRelationSeed[] }[];
// Junction fields create relations to junction objects (inverses auto-created)
junctionFields?: JunctionFieldSeed[];
// Configure junction settings on fields after all relations exist
junctionConfigs?: JunctionConfigSeed[];
}
> = {
private readonly workspaceConfigs: Record<string, WorkspaceSeedConfig> = {
[SEED_APPLE_WORKSPACE_ID]: {
objects: [
{ seed: ROCKET_CUSTOM_OBJECT_SEED },
@@ -178,13 +175,14 @@ export class DevSeederMetadataService {
},
};
public async seed({
dataSourceMetadata,
workspaceId,
}: {
dataSourceMetadata: DataSourceEntity;
workspaceId: string;
}) {
private getLightConfig(config: WorkspaceSeedConfig): WorkspaceSeedConfig {
return {
objects: [],
fields: [],
};
}
private getConfig(workspaceId: string, light: boolean): WorkspaceSeedConfig {
const config = this.workspaceConfigs[workspaceId];
if (!config) {
@@ -193,6 +191,20 @@ export class DevSeederMetadataService {
);
}
return light ? this.getLightConfig(config) : config;
}
public async seed({
dataSourceMetadata,
workspaceId,
light = false,
}: {
dataSourceMetadata: DataSourceEntity;
workspaceId: string;
light?: boolean;
}) {
const config = this.getConfig(workspaceId, light);
for (const obj of config.objects) {
await this.seedCustomObject({
dataSourceId: dataSourceMetadata.id,
@@ -266,14 +278,14 @@ export class DevSeederMetadataService {
});
}
public async seedRelations({ workspaceId }: { workspaceId: string }) {
const config = this.workspaceConfigs[workspaceId];
if (!config) {
throw new Error(
`Workspace configuration not found for workspaceId: ${workspaceId}`,
);
}
public async seedRelations({
workspaceId,
light = false,
}: {
workspaceId: string;
light?: boolean;
}) {
const config = this.getConfig(workspaceId, light);
// 1. Seed morph relations (creates inverses on target objects)
let maps = await this.getFreshFlatMaps(workspaceId);
@@ -287,7 +299,6 @@ export class DevSeederMetadataService {
}
// 2. Seed junction fields (creates relations + inverses on junction objects)
// Use same maps for all - matches original working behavior
maps = await this.getFreshFlatMaps(workspaceId);
for (const field of config.junctionFields ?? []) {
@@ -43,7 +43,11 @@ export class DevSeederService {
private readonly coreDataSource: DataSource,
) {}
public async seedDev(workspaceId: SeededWorkspacesIds): Promise<void> {
public async seedDev(
workspaceId: SeededWorkspacesIds,
options?: { light?: boolean },
): Promise<void> {
const light = options?.light ?? false;
const isBillingEnabled = this.twentyConfigService.get('IS_BILLING_ENABLED');
const appVersion = this.twentyConfigService.get('APP_VERSION');
@@ -89,16 +93,19 @@ export class DevSeederService {
await this.devSeederMetadataService.seed({
dataSourceMetadata,
workspaceId,
light,
});
await this.devSeederMetadataService.seedRelations({
workspaceId,
light,
});
await this.devSeederPermissionsService.initPermissions({
workspaceId,
twentyStandardFlatApplication,
workspaceCustomFlatApplication,
light,
});
await seedPageLayouts(
@@ -148,6 +155,7 @@ export class DevSeederService {
schemaName: dataSourceMetadata.schema,
workspaceId,
featureFlags: featureFlagsMap,
light,
});
await this.workspaceCacheStorageService.flush(workspaceId, undefined);