feat(*): allow to select auth providers + add multiworkspace with subdomain management (#8656)

## Summary
Add support for multi-workspace feature and adjust configurations and
states accordingly.
- Introduced new state isMultiWorkspaceEnabledState.
- Updated ClientConfigProviderEffect component to handle
multi-workspace.
- Modified GraphQL schema and queries to include multi-workspace related
configurations.
- Adjusted server environment variables and their respective
documentation to support multi-workspace toggle.
- Updated server-side logic to handle new multi-workspace configurations
and conditions.
This commit is contained in:
Antoine Moreaux
2024-12-03 19:06:28 +01:00
committed by GitHub
parent 9a65e80566
commit 7943141d03
167 changed files with 5180 additions and 1901 deletions

View File

@ -0,0 +1,123 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository, In } from 'typeorm';
import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { BaseCommandOptions } from 'src/database/commands/base.command';
// For DX only
type WorkspaceId = string;
type Subdomain = string;
@Command({
name: 'feat-0.34:add-subdomain-to-workspace',
description: 'Add a default subdomain to each workspace',
})
export class GenerateDefaultSubdomainCommand extends ActiveWorkspacesCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
) {
super(workspaceRepository);
}
private generatePayloadForQuery({
id,
subdomain,
domainName,
displayName,
}: Workspace) {
const result = { id, subdomain };
if (domainName) {
const subdomain = domainName.split('.')[0];
if (subdomain.length > 0) {
result.subdomain = subdomain;
}
}
if (!domainName && displayName) {
const displayNameWords = displayName.match(/(\w| |\d)+/);
if (displayNameWords) {
result.subdomain = displayNameWords
.join('-')
.replace(/ /g, '')
.toLowerCase();
}
}
return result;
}
private groupBySubdomainName(
acc: Record<Subdomain, Array<WorkspaceId>>,
workspace: Workspace,
) {
const payload = this.generatePayloadForQuery(workspace);
acc[payload.subdomain] = acc[payload.subdomain]
? acc[payload.subdomain].concat([payload.id])
: [payload.id];
return acc;
}
private async deduplicateAndSave(
subdomain: Subdomain,
workspaceIds: Array<WorkspaceId>,
options: BaseCommandOptions,
) {
for (const [index, workspaceId] of workspaceIds.entries()) {
const subdomainDeduplicated =
index === 0 ? subdomain : `${subdomain}-${index}`;
this.logger.log(
`Updating workspace ${workspaceId} with subdomain ${subdomainDeduplicated}`,
);
if (!options.dryRun) {
await this.workspaceRepository.update(workspaceId, {
subdomain: subdomainDeduplicated,
});
}
}
}
async executeActiveWorkspacesCommand(
passedParam: string[],
options: BaseCommandOptions,
activeWorkspaceIds: string[],
): Promise<void> {
const workspaces = await this.workspaceRepository.find(
activeWorkspaceIds.length > 0
? {
where: {
id: In(activeWorkspaceIds),
},
}
: undefined,
);
if (workspaces.length === 0) {
this.logger.log('No workspaces found');
return;
}
const workspaceBySubdomain = Object.entries(
workspaces.reduce(
(acc, workspace) => this.groupBySubdomainName(acc, workspace),
{} as ReturnType<typeof this.groupBySubdomainName>,
),
);
for (const [subdomain, workspaceIds] of workspaceBySubdomain) {
await this.deduplicateAndSave(subdomain, workspaceIds, options);
}
}
}

View File

@ -0,0 +1,38 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { GenerateDefaultSubdomainCommand } from 'src/database/commands/upgrade-version/0-34/0-34-generate-subdomain.command';
interface UpdateTo0_34CommandOptions {
workspaceId?: string;
}
@Command({
name: 'upgrade-0.34',
description: 'Upgrade to 0.34',
})
export class UpgradeTo0_34Command extends ActiveWorkspacesCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
private readonly generateDefaultSubdomainCommand: GenerateDefaultSubdomainCommand,
) {
super(workspaceRepository);
}
async executeActiveWorkspacesCommand(
passedParam: string[],
options: UpdateTo0_34CommandOptions,
workspaceIds: string[],
): Promise<void> {
await this.generateDefaultSubdomainCommand.executeActiveWorkspacesCommand(
passedParam,
options,
workspaceIds,
);
}
}

View File

@ -0,0 +1,25 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { SearchModule } from 'src/engine/metadata-modules/search/search.module';
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module';
import { UpgradeTo0_34Command } from 'src/database/commands/upgrade-version/0-34/0-34-upgrade-version.command';
@Module({
imports: [
TypeOrmModule.forFeature([Workspace], 'core'),
TypeOrmModule.forFeature(
[ObjectMetadataEntity, FieldMetadataEntity],
'metadata',
),
WorkspaceSyncMetadataCommandsModule,
SearchModule,
WorkspaceMigrationRunnerModule,
],
providers: [UpgradeTo0_34Command],
})
export class UpgradeTo0_33CommandModule {}

View File

@ -23,6 +23,7 @@ export const seedWorkspaces = async (
| 'domainName'
| 'inviteHash'
| 'logo'
| 'subdomain'
| 'activationStatus'
>;
} = {
@ -30,6 +31,7 @@ export const seedWorkspaces = async (
id: workspaceId,
displayName: 'Apple',
domainName: 'apple.dev',
subdomain: 'apple',
inviteHash: 'apple.dev-invite-hash',
logo: 'https://twentyhq.github.io/placeholder-images/workspaces/apple-logo.png',
activationStatus: WorkspaceActivationStatus.ACTIVE,
@ -38,6 +40,7 @@ export const seedWorkspaces = async (
id: workspaceId,
displayName: 'Acme',
domainName: 'acme.dev',
subdomain: 'acme',
inviteHash: 'acme.dev-invite-hash',
logo: 'https://logos-world.net/wp-content/uploads/2022/05/Acme-Logo-700x394.png',
activationStatus: WorkspaceActivationStatus.ACTIVE,
@ -51,6 +54,7 @@ export const seedWorkspaces = async (
'id',
'displayName',
'domainName',
'subdomain',
'inviteHash',
'logo',
'activationStatus',

View File

@ -0,0 +1,26 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddSubdomainToWorkspace1730137590546
implements MigrationInterface
{
name = 'AddSubdomainToWorkspace1730137590546';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."workspace" ADD "subdomain" varchar NULL`,
);
await queryRunner.query(`UPDATE "core"."workspace" SET "subdomain" = "id"`);
await queryRunner.query(
`ALTER TABLE "core"."workspace" ALTER COLUMN "subdomain" SET NOT NULL`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX workspace_subdomain_unique_index ON "core"."workspace" (subdomain)`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."workspace" DROP COLUMN "subdomain"`,
);
}
}

View File

@ -0,0 +1,31 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddAuthProvidersColumnsToWorkspace1730298416367
implements MigrationInterface
{
name = 'AddAuthProvidersColumnsToWorkspace1730298416367';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."workspace" ADD "isMicrosoftAuthEnabled" BOOLEAN DEFAULT false`,
);
await queryRunner.query(
`ALTER TABLE "core"."workspace" ADD "isGoogleAuthEnabled" BOOLEAN DEFAULT true`,
);
await queryRunner.query(
`ALTER TABLE "core"."workspace" ADD "isPasswordAuthEnabled" BOOLEAN DEFAULT true`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."workspace" DROP COLUMN "isMicrosoftAuthEnabled"`,
);
await queryRunner.query(
`ALTER TABLE "core"."workspace" DROP COLUMN "isGoogleAuthEnabled"`,
);
await queryRunner.query(
`ALTER TABLE "core"."workspace" DROP COLUMN "isPasswordAuthEnabled"`,
);
}
}