Twenty standard and workspace custom applications 1/3 (#15625)
# Introduction related to https://github.com/twentyhq/core-team-issues/issues/1833 In this PR we're starting the sync-metadata and standardIds deprecation by introducing `twenty-standard` application that will regroup every standard object such as company and opportunities. But also the `custom-workspace-application` which is an app created at the same time as a workspace and that will regroup everything configure within the workspace ( custom objects fields etc ) ## What's done On both new workspace and seeded workspace creation: - Creating a custom workspace app - Creating a twenty standard app - Refactored the seed core schema and workspace creation to be run within a transaction in order to handle circular dependency foreignkey requirements ( which is deferred for app toward workspace ) - Updated workspace entity to have a custom workspace relation ( nullable for the moment until we implem an upgrade command to handle retro comp ) - Integration testing on user, workspace creation deletion and expected default apps creation - ~~Soft deleted user on `deleteUser`~~ Done by marie and rebased on it ## What's next - Update seeder to propagate the `twenty-standard` workspace `applicationId` to every standard synchronized entities ( cheap and fast iteration through the about to be deprecated sync-metadata as an easy way to synchronize standards metadata entities ). - Update seeder to propagate the `custom-workspace-application` workspace `applicationId` to anything custom ( `pets` and `rockets` ) - Prepend `custom-workspace-application` `applicationId` to every metadata API operations ( create a specific cache etc ) - Upgrade command on all existing workspace to create a custom app and associate its applicationId to any existing custom entities - Make `universalIdentifier` and `applicationId` required for any syncable entity
This commit is contained in:
@@ -222,6 +222,7 @@ export type Application = {
|
||||
name: Scalars['String'];
|
||||
objects: Array<Object>;
|
||||
serverlessFunctions: Array<ServerlessFunction>;
|
||||
universalIdentifier: Scalars['String'];
|
||||
version: Scalars['String'];
|
||||
};
|
||||
|
||||
@@ -4802,6 +4803,7 @@ export type Workspace = {
|
||||
viewGroups?: Maybe<Array<CoreViewGroup>>;
|
||||
viewSorts?: Maybe<Array<CoreViewSort>>;
|
||||
views?: Maybe<Array<CoreView>>;
|
||||
workspaceCustomApplicationId?: Maybe<Scalars['String']>;
|
||||
workspaceMembersCount?: Maybe<Scalars['Float']>;
|
||||
workspaceUrls: WorkspaceUrls;
|
||||
};
|
||||
|
||||
@@ -222,6 +222,7 @@ export type Application = {
|
||||
name: Scalars['String'];
|
||||
objects: Array<Object>;
|
||||
serverlessFunctions: Array<ServerlessFunction>;
|
||||
universalIdentifier: Scalars['String'];
|
||||
version: Scalars['String'];
|
||||
};
|
||||
|
||||
@@ -4630,6 +4631,7 @@ export type Workspace = {
|
||||
viewGroups?: Maybe<Array<CoreViewGroup>>;
|
||||
viewSorts?: Maybe<Array<CoreViewSort>>;
|
||||
views?: Maybe<Array<CoreView>>;
|
||||
workspaceCustomApplicationId?: Maybe<Scalars['String']>;
|
||||
workspaceMembersCount?: Maybe<Scalars['Float']>;
|
||||
workspaceUrls: WorkspaceUrls;
|
||||
};
|
||||
|
||||
+4
-2
@@ -1,14 +1,16 @@
|
||||
import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard';
|
||||
import { SettingsAdminVersionDisplay } from '@/settings/admin-panel/components/SettingsAdminVersionDisplay';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconCircleDot, IconStatusChange } from 'twenty-ui/display';
|
||||
import type { Application } from '~/generated/graphql';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const SettingsApplicationVersionContainer = ({
|
||||
application,
|
||||
}: {
|
||||
application?: Omit<Application, 'objects'> & { objects: { id: string }[] };
|
||||
application?: Omit<Application, 'objects' | 'universalIdentifier'> & {
|
||||
objects: { id: string }[];
|
||||
};
|
||||
}) => {
|
||||
const loading = !isDefined(application);
|
||||
|
||||
|
||||
+6
-4
@@ -1,15 +1,17 @@
|
||||
import type { Application } from '~/generated/graphql';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import type { Application } from '~/generated/graphql';
|
||||
import { SettingsApplicationVersionContainer } from '~/pages/settings/applications/components/SettingsApplicationVersionContainer';
|
||||
|
||||
export const SettingsApplicationDetailAboutTab = ({
|
||||
application,
|
||||
}: {
|
||||
application?: Omit<Application, 'objects'> & { objects: { id: string }[] };
|
||||
application?: Omit<Application, 'objects' | 'universalIdentifier'> & {
|
||||
objects: { id: string }[];
|
||||
};
|
||||
}) => {
|
||||
if (!isDefined(application)) {
|
||||
return null;
|
||||
|
||||
+3
-1
@@ -12,7 +12,9 @@ import { SettingsObjectTable } from '~/pages/settings/data-model/SettingsObjectT
|
||||
export const SettingsApplicationDetailContentTab = ({
|
||||
application,
|
||||
}: {
|
||||
application?: Omit<Application, 'objects'> & { objects: { id: string }[] };
|
||||
application?: Omit<Application, 'objects' | 'universalIdentifier'> & {
|
||||
objects: { id: string }[];
|
||||
};
|
||||
}) => {
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
|
||||
+5
-3
@@ -1,12 +1,14 @@
|
||||
import type { Application } from '~/generated/graphql';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { SettingsApplicationDetailEnvironmentVariablesTable } from '~/pages/settings/applications/tabs/SettingsApplicationDetailEnvironmentVariablesTable';
|
||||
import type { Application } from '~/generated/graphql';
|
||||
import { useUpdateOneApplicationVariable } from '~/pages/settings/applications/hooks/useUpdateOneApplicationVariable';
|
||||
import { SettingsApplicationDetailEnvironmentVariablesTable } from '~/pages/settings/applications/tabs/SettingsApplicationDetailEnvironmentVariablesTable';
|
||||
|
||||
export const SettingsApplicationDetailSettingsTab = ({
|
||||
application,
|
||||
}: {
|
||||
application?: Omit<Application, 'objects'> & { objects: { id: string }[] };
|
||||
application?: Omit<Application, 'objects' | 'universalIdentifier'> & {
|
||||
objects: { id: string }[];
|
||||
};
|
||||
}) => {
|
||||
const { updateOneApplicationVariable } = useUpdateOneApplicationVariable();
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ SIGN_IN_PREFILLED=true
|
||||
EXCEPTION_HANDLER_DRIVER=CONSOLE
|
||||
SENTRY_DSN=https://ba869cb8fd72d5faeb6643560939cee0@o4505516959793152.ingest.sentry.io/4506660900306944
|
||||
MUTATION_MAXIMUM_RECORD_AFFECTED=100
|
||||
|
||||
IS_MULTIWORKSPACE_ENABLED=true
|
||||
FRONTEND_URL=http://localhost:3001
|
||||
|
||||
AUTH_GOOGLE_ENABLED=false
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Command, CommandRunner } from 'nest-commander';
|
||||
import {
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
SEED_YCOMBINATOR_WORKSPACE_ID,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
import { DevSeederService } from 'src/engine/workspace-manager/dev-seeder/services/dev-seeder.service';
|
||||
@Command({
|
||||
name: 'workspace:seed:dev',
|
||||
@@ -13,7 +13,10 @@ import { DevSeederService } from 'src/engine/workspace-manager/dev-seeder/servic
|
||||
'Seed workspace with initial data. This command is intended for development only.',
|
||||
})
|
||||
export class DataSeedWorkspaceCommand extends CommandRunner {
|
||||
workspaceIds = [SEED_APPLE_WORKSPACE_ID, SEED_YCOMBINATOR_WORKSPACE_ID];
|
||||
workspaceIds = [
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
SEED_YCOMBINATOR_WORKSPACE_ID,
|
||||
] as const;
|
||||
private readonly logger = new Logger(DataSeedWorkspaceCommand.name);
|
||||
|
||||
constructor(private readonly devSeederService: DevSeederService) {
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
type RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/twenty-standard-applications';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-11:create-twenty-standard-application',
|
||||
description:
|
||||
'Create twenty-standard application for workspaces that do not have them',
|
||||
})
|
||||
export class CreateTwentyStandardApplicationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
@InjectRepository(ApplicationEntity)
|
||||
private readonly applicationRepository: Repository<ApplicationEntity>,
|
||||
private readonly applicationService: ApplicationService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Checking standard applications for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const existingApplications = await this.applicationRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
universalIdentifier: In([TWENTY_STANDARD_APPLICATION]),
|
||||
},
|
||||
});
|
||||
|
||||
const existingIds = new Set(
|
||||
existingApplications.map((app) => app.universalIdentifier),
|
||||
);
|
||||
|
||||
if (existingIds.has(TWENTY_STANDARD_APPLICATION.universalIdentifier)) {
|
||||
this.logger.log(
|
||||
`Skipping twenty standard application as it already exists`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(`About to seed twenty standard application`);
|
||||
if (options.dryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.applicationService.createTwentyStandardApplication({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
this.logger.log(`Successfully seeded twenty standard`);
|
||||
} catch (e) {
|
||||
this.logger.error(`Failed to seed twenty standard`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -3,6 +3,9 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { CleanOrphanedRoleTargetsCommand } from 'src/database/commands/upgrade-version-command/1-11/1-11-clean-orphaned-role-targets.command';
|
||||
import { CleanOrphanedUserWorkspacesCommand } from 'src/database/commands/upgrade-version-command/1-11/1-11-clean-orphaned-user-workspaces.command';
|
||||
import { CreateTwentyStandardApplicationCommand } from 'src/database/commands/upgrade-version-command/1-11/1-11-create-twenty-standard-application.command';
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
@@ -22,16 +25,20 @@ import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-sc
|
||||
ViewEntity,
|
||||
UserWorkspaceEntity,
|
||||
RoleTargetsEntity,
|
||||
ApplicationEntity,
|
||||
]),
|
||||
WorkspaceSchemaManagerModule,
|
||||
ApplicationModule,
|
||||
],
|
||||
providers: [
|
||||
CleanOrphanedUserWorkspacesCommand,
|
||||
CleanOrphanedRoleTargetsCommand,
|
||||
CreateTwentyStandardApplicationCommand,
|
||||
],
|
||||
exports: [
|
||||
CleanOrphanedUserWorkspacesCommand,
|
||||
CleanOrphanedRoleTargetsCommand,
|
||||
CreateTwentyStandardApplicationCommand,
|
||||
],
|
||||
})
|
||||
export class V1_11_UpgradeVersionCommandModule {}
|
||||
|
||||
+3
-1
@@ -21,6 +21,7 @@ import { RegenerateSearchVectorsCommand } from 'src/database/commands/upgrade-ve
|
||||
import { SeedDashboardViewCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-seed-dashboard-view.command';
|
||||
import { CleanOrphanedRoleTargetsCommand } from 'src/database/commands/upgrade-version-command/1-11/1-11-clean-orphaned-role-targets.command';
|
||||
import { CleanOrphanedUserWorkspacesCommand } from 'src/database/commands/upgrade-version-command/1-11/1-11-clean-orphaned-user-workspaces.command';
|
||||
import { CreateTwentyStandardApplicationCommand } from 'src/database/commands/upgrade-version-command/1-11/1-11-create-twenty-standard-application.command';
|
||||
import { FixLabelIdentifierPositionAndVisibilityCommand } from 'src/database/commands/upgrade-version-command/1-6/1-6-fix-label-identifier-position-and-visibility.command';
|
||||
import { BackfillWorkflowManualTriggerAvailabilityCommand } from 'src/database/commands/upgrade-version-command/1-7/1-7-backfill-workflow-manual-trigger-availability.command';
|
||||
import { DeduplicateUniqueFieldsCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-deduplicate-unique-fields.command';
|
||||
@@ -75,6 +76,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
// 1.11 Commands
|
||||
protected readonly cleanOrphanedUserWorkspacesCommand: CleanOrphanedUserWorkspacesCommand,
|
||||
protected readonly cleanOrphanedRoleTargetsCommand: CleanOrphanedRoleTargetsCommand,
|
||||
protected readonly seedStandardApplicationsCommand: CreateTwentyStandardApplicationCommand,
|
||||
) {
|
||||
super(
|
||||
workspaceRepository,
|
||||
@@ -124,7 +126,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
};
|
||||
|
||||
const commands_1110: VersionCommands = {
|
||||
beforeSyncMetadata: [],
|
||||
beforeSyncMetadata: [this.seedStandardApplicationsCommand],
|
||||
afterSyncMetadata: [
|
||||
this.cleanOrphanedUserWorkspacesCommand,
|
||||
this.cleanOrphanedRoleTargetsCommand,
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { type MigrationInterface, type QueryRunner } from 'typeorm';
|
||||
|
||||
export class NullableApplicationServerlessFunctionLayer1762333916255
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'NullableApplicationServerlessFunctionLayer1762333916255';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."application" ALTER COLUMN "serverlessFunctionLayerId" DROP NOT NULL`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."application" ALTER COLUMN "serverlessFunctionLayerId" SET NOT NULL`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { type MigrationInterface, type QueryRunner } from 'typeorm';
|
||||
|
||||
export class MakeApplicationWorkspaceFkDeferrable1762339932345
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'MakeApplicationWorkspaceFkDeferrable1762339932345';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."application" DROP CONSTRAINT IF EXISTS "FK_08d1d5e33c2a3ce7c140e9b335b"`,
|
||||
);
|
||||
|
||||
// Recreate application.workspaceId FK as DEFERRABLE INITIALLY DEFERRED
|
||||
// This is the ONLY deferrable constraint - needed because we create application before workspace
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."application" ADD CONSTRAINT "FK_08d1d5e33c2a3ce7c140e9b335b" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."application" DROP CONSTRAINT IF EXISTS "FK_08d1d5e33c2a3ce7c140e9b335b"`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."application" ADD CONSTRAINT "FK_08d1d5e33c2a3ce7c140e9b335b" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { type MigrationInterface, type QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddWorkspaceCustomApplicationIdColumn1762343994716
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddWorkspaceCustomApplicationIdColumn1762343994716';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."workspace" ADD "workspaceCustomApplicationId" uuid`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "core"."IDX_APPLICATION_UNIVERSAL_IDENTIFIER_WORKSPACE_ID_UNIQUE"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."application" ALTER COLUMN "universalIdentifier" SET NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "IDX_APPLICATION_UNIVERSAL_IDENTIFIER_WORKSPACE_ID_UNIQUE" ON "core"."application" ("universalIdentifier", "workspaceId") WHERE "deletedAt" IS NULL AND "universalIdentifier" IS NOT NULL`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "core"."IDX_APPLICATION_UNIVERSAL_IDENTIFIER_WORKSPACE_ID_UNIQUE"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."application" ALTER COLUMN "universalIdentifier" DROP NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "IDX_APPLICATION_UNIVERSAL_IDENTIFIER_WORKSPACE_ID_UNIQUE" ON "core"."application" ("universalIdentifier", "workspaceId") WHERE (("deletedAt" IS NULL) AND ("universalIdentifier" IS NOT NULL"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."workspace" DROP COLUMN "workspaceCustomApplicationId"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { type MigrationInterface, type QueryRunner } from 'typeorm';
|
||||
|
||||
export class WorkspaceCustomApplicationIdForeignKey1762437814771
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'WorkspaceCustomApplicationIdForeignKey1762437814771';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."workspace" ADD CONSTRAINT "FK_3b1acb13a5dac9956d1a4b32755" FOREIGN KEY ("workspaceCustomApplicationId") REFERENCES "core"."application"("id") ON DELETE RESTRICT ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."workspace" DROP CONSTRAINT "FK_3b1acb13a5dac9956d1a4b32755"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+55
-53
@@ -19,27 +19,27 @@ import {
|
||||
ServerlessFunctionTriggerManifest,
|
||||
} from 'src/engine/core-modules/application/types/application.types';
|
||||
import { ApplicationVariableEntityService } from 'src/engine/core-modules/applicationVariable/application-variable.service';
|
||||
import { Sources } from 'src/engine/core-modules/file-storage/types/source.type';
|
||||
import { CronTriggerV2Service } from 'src/engine/metadata-modules/cron-trigger/services/cron-trigger-v2.service';
|
||||
import { FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { DatabaseEventTriggerV2Service } from 'src/engine/metadata-modules/database-event-trigger/services/database-event-trigger-v2.service';
|
||||
import { FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type';
|
||||
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
import { FieldMetadataServiceV2 } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2';
|
||||
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { getFlatEntitiesByApplicationId } from 'src/engine/metadata-modules/flat-entity/utils/get-flat-entities-by-application-id.util';
|
||||
import { getSubFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-maps-or-throw.util';
|
||||
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
|
||||
import { RouteTriggerV2Service } from 'src/engine/metadata-modules/route-trigger/services/route-trigger-v2.service';
|
||||
import { FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type';
|
||||
import { ServerlessFunctionLayerService } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.service';
|
||||
import { ServerlessFunctionV2Service } from 'src/engine/metadata-modules/serverless-function/services/serverless-function-v2.service';
|
||||
import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
|
||||
import { Sources } from 'src/engine/core-modules/file-storage/types/source.type';
|
||||
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { FieldMetadataServiceV2 } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2';
|
||||
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
@Injectable()
|
||||
export class ApplicationSyncService {
|
||||
@@ -81,13 +81,22 @@ export class ApplicationSyncService {
|
||||
applicationId: application.id,
|
||||
});
|
||||
|
||||
await this.syncServerlessFunctions({
|
||||
serverlessFunctionsToSync: manifest.serverlessFunctions,
|
||||
code: manifest.sources,
|
||||
workspaceId,
|
||||
applicationId: application.id,
|
||||
serverlessFunctionLayerId: application.serverlessFunctionLayerId,
|
||||
});
|
||||
if (manifest.serverlessFunctions.length > 0) {
|
||||
if (!isDefined(application.serverlessFunctionLayerId)) {
|
||||
throw new ApplicationException(
|
||||
`Failed to sync serverless function, could not find a serverless function layer.`,
|
||||
ApplicationExceptionCode.FIELD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
await this.syncServerlessFunctions({
|
||||
serverlessFunctionsToSync: manifest.serverlessFunctions,
|
||||
code: manifest.sources,
|
||||
workspaceId,
|
||||
applicationId: application.id,
|
||||
serverlessFunctionLayerId: application.serverlessFunctionLayerId,
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.log('✅ Application sync from manifest completed');
|
||||
}
|
||||
@@ -100,57 +109,46 @@ export class ApplicationSyncService {
|
||||
}: ApplicationInput & {
|
||||
workspaceId: string;
|
||||
}): Promise<ApplicationEntity> {
|
||||
const application = await this.applicationService.findByUniversalIdentifier(
|
||||
manifest.application.universalIdentifier,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const name = manifest.application.displayName ?? packageJson.name;
|
||||
|
||||
if (!isDefined(application)) {
|
||||
const serverlessFunctionLayer =
|
||||
await this.serverlessFunctionLayerService.create(
|
||||
{
|
||||
packageJson,
|
||||
yarnLock,
|
||||
},
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const application = await this.applicationService.create({
|
||||
const application =
|
||||
(await this.applicationService.findByUniversalIdentifier({
|
||||
universalIdentifier: manifest.application.universalIdentifier,
|
||||
workspaceId,
|
||||
})) ??
|
||||
(await this.applicationService.create({
|
||||
universalIdentifier: manifest.application.universalIdentifier,
|
||||
name,
|
||||
description: manifest.application.description,
|
||||
version: packageJson.version,
|
||||
sourcePath: 'cli-sync', // Placeholder for CLI-synced apps
|
||||
serverlessFunctionLayerId: serverlessFunctionLayer.id,
|
||||
serverlessFunctionLayerId: null,
|
||||
workspaceId,
|
||||
});
|
||||
}));
|
||||
|
||||
await this.applicationVariableService.upsertManyApplicationVariableEntities(
|
||||
let serverlessFunctionLayerId = application.serverlessFunctionLayerId;
|
||||
|
||||
if (manifest.serverlessFunctions.length > 0) {
|
||||
if (!isDefined(serverlessFunctionLayerId)) {
|
||||
serverlessFunctionLayerId = (
|
||||
await this.serverlessFunctionLayerService.create(
|
||||
{
|
||||
packageJson,
|
||||
yarnLock,
|
||||
},
|
||||
workspaceId,
|
||||
)
|
||||
).id;
|
||||
}
|
||||
|
||||
await this.serverlessFunctionLayerService.update(
|
||||
serverlessFunctionLayerId,
|
||||
{
|
||||
applicationVariables: manifest.application.applicationVariables,
|
||||
applicationId: application.id,
|
||||
packageJson,
|
||||
yarnLock,
|
||||
},
|
||||
);
|
||||
|
||||
return application;
|
||||
}
|
||||
|
||||
await this.serverlessFunctionLayerService.update(
|
||||
application.serverlessFunctionLayerId,
|
||||
{
|
||||
packageJson,
|
||||
yarnLock,
|
||||
},
|
||||
);
|
||||
|
||||
await this.applicationService.update(application.id, {
|
||||
name,
|
||||
description: manifest.application.description,
|
||||
version: packageJson.version,
|
||||
});
|
||||
|
||||
await this.applicationVariableService.upsertManyApplicationVariableEntities(
|
||||
{
|
||||
applicationVariables: manifest.application.applicationVariables,
|
||||
@@ -158,7 +156,12 @@ export class ApplicationSyncService {
|
||||
},
|
||||
);
|
||||
|
||||
return application;
|
||||
return await this.applicationService.update(application.id, {
|
||||
name,
|
||||
description: manifest.application.description,
|
||||
version: packageJson.version,
|
||||
serverlessFunctionLayerId,
|
||||
});
|
||||
}
|
||||
|
||||
private async syncFields({
|
||||
@@ -947,8 +950,7 @@ export class ApplicationSyncService {
|
||||
);
|
||||
|
||||
const application = await this.applicationService.findByUniversalIdentifier(
|
||||
applicationUniversalIdentifier,
|
||||
workspaceId,
|
||||
{ universalIdentifier: applicationUniversalIdentifier, workspaceId },
|
||||
);
|
||||
|
||||
if (!isDefined(application)) {
|
||||
|
||||
@@ -35,8 +35,8 @@ export class ApplicationEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
universalIdentifier?: string;
|
||||
@Column({ nullable: false, type: 'uuid' })
|
||||
universalIdentifier: string;
|
||||
|
||||
@Column({ nullable: false, type: 'text' })
|
||||
name: string;
|
||||
@@ -56,8 +56,8 @@ export class ApplicationEntity {
|
||||
@Column({ nullable: false, type: 'uuid' })
|
||||
workspaceId: string;
|
||||
|
||||
@Column({ nullable: false, type: 'uuid' })
|
||||
serverlessFunctionLayerId: string;
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
serverlessFunctionLayerId: string | null;
|
||||
|
||||
@ManyToOne(() => WorkspaceEntity, {
|
||||
onDelete: 'CASCADE',
|
||||
|
||||
@@ -36,6 +36,7 @@ import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/
|
||||
WorkspaceMigrationV2Module,
|
||||
PermissionsModule,
|
||||
],
|
||||
exports: [ApplicationService],
|
||||
providers: [ApplicationResolver, ApplicationService, ApplicationSyncService],
|
||||
})
|
||||
export class ApplicationModule {}
|
||||
|
||||
@@ -2,15 +2,15 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
import { type QueryRunner, Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { PackageJson } from 'src/engine/core-modules/application/types/application.types';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import {
|
||||
ApplicationException,
|
||||
ApplicationExceptionCode,
|
||||
} from 'src/engine/core-modules/application/application.exception';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/twenty-standard-applications';
|
||||
|
||||
@Injectable()
|
||||
export class ApplicationService {
|
||||
@@ -64,10 +64,13 @@ export class ApplicationService {
|
||||
});
|
||||
}
|
||||
|
||||
async findByUniversalIdentifier(
|
||||
universalIdentifier: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
async findByUniversalIdentifier({
|
||||
universalIdentifier,
|
||||
workspaceId,
|
||||
}: {
|
||||
universalIdentifier: string;
|
||||
workspaceId: string;
|
||||
}) {
|
||||
return this.applicationRepository.findOne({
|
||||
where: {
|
||||
universalIdentifier,
|
||||
@@ -76,34 +79,51 @@ export class ApplicationService {
|
||||
});
|
||||
}
|
||||
|
||||
async create(data: {
|
||||
universalIdentifier?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
version?: string;
|
||||
serverlessFunctionLayerId: string;
|
||||
sourcePath: string;
|
||||
workspaceId: string;
|
||||
}): Promise<ApplicationEntity> {
|
||||
async createTwentyStandardApplication(
|
||||
{
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
return await this.create(
|
||||
{
|
||||
...TWENTY_STANDARD_APPLICATION,
|
||||
serverlessFunctionLayerId: null,
|
||||
workspaceId,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
async create(
|
||||
data: {
|
||||
universalIdentifier?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
version?: string;
|
||||
serverlessFunctionLayerId: string | null;
|
||||
sourcePath: string;
|
||||
workspaceId: string;
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
): Promise<ApplicationEntity> {
|
||||
const application = this.applicationRepository.create({
|
||||
...data,
|
||||
sourceType: 'local',
|
||||
});
|
||||
|
||||
if (queryRunner) {
|
||||
return queryRunner.manager.save(ApplicationEntity, application);
|
||||
}
|
||||
|
||||
return this.applicationRepository.save(application);
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
data: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
version?: string;
|
||||
sourcePath?: string;
|
||||
packageJson?: PackageJson;
|
||||
yarnLock?: string;
|
||||
packageChecksum?: string;
|
||||
},
|
||||
data: Parameters<typeof this.applicationRepository.update>[1],
|
||||
): Promise<ApplicationEntity> {
|
||||
await this.applicationRepository.update({ id }, data);
|
||||
|
||||
@@ -117,10 +137,10 @@ export class ApplicationService {
|
||||
}
|
||||
|
||||
async delete(universalIdentifier: string, workspaceId: string) {
|
||||
const application = await this.findByUniversalIdentifier(
|
||||
const application = await this.findByUniversalIdentifier({
|
||||
universalIdentifier,
|
||||
workspaceId,
|
||||
);
|
||||
});
|
||||
|
||||
if (!isDefined(application)) {
|
||||
throw new Error(`Application does not exist`);
|
||||
|
||||
@@ -3,10 +3,10 @@ import { Field, ObjectType } from '@nestjs/graphql';
|
||||
import { IsNotEmpty, IsString, IsUUID } from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
|
||||
import { ApplicationVariableEntityDTO } from 'src/engine/core-modules/applicationVariable/dtos/application-variable.dto';
|
||||
import { AgentDTO } from 'src/engine/metadata-modules/agent/dtos/agent.dto';
|
||||
import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
|
||||
import { ApplicationVariableEntityDTO } from 'src/engine/core-modules/applicationVariable/dtos/application-variable.dto';
|
||||
import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
|
||||
|
||||
@ObjectType('Application')
|
||||
export class ApplicationDTO {
|
||||
@@ -38,4 +38,8 @@ export class ApplicationDTO {
|
||||
|
||||
@Field(() => [ApplicationVariableEntityDTO])
|
||||
applicationVariables: ApplicationVariableEntityDTO[];
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
universalIdentifier: string;
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works
|
||||
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
|
||||
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
||||
import { MessagingFolderSyncManagerModule } from 'src/modules/messaging/message-folder-manager/messaging-folder-sync-manager.module';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
|
||||
import { TwoFactorAuthenticationMethodEntity } from '../two-factor-authentication/entities/two-factor-authentication-method.entity';
|
||||
import { TwoFactorAuthenticationModule } from '../two-factor-authentication/two-factor-authentication.module';
|
||||
@@ -113,6 +114,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
||||
AuditModule,
|
||||
SubdomainManagerModule,
|
||||
DomainServerConfigModule,
|
||||
ApplicationModule,
|
||||
],
|
||||
controllers: [
|
||||
GoogleAuthController,
|
||||
|
||||
-409
@@ -1,409 +0,0 @@
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { type Repository } from 'typeorm';
|
||||
|
||||
import { type AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
import {
|
||||
type AuthProviderWithPasswordType,
|
||||
type ExistingUserOrPartialUserWithPicture,
|
||||
type SignInUpBaseParams,
|
||||
} from 'src/engine/core-modules/auth/types/signInUp.type';
|
||||
import { SubdomainManagerService } from 'src/engine/core-modules/domain/subdomain-manager/services/subdomain-manager.service';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
|
||||
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { type UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
|
||||
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||
|
||||
jest.mock('src/utils/image', () => {
|
||||
return {
|
||||
getImageBufferFromUrl: () => Promise.resolve(Buffer.from('')),
|
||||
};
|
||||
});
|
||||
|
||||
describe('SignInUpService', () => {
|
||||
let service: SignInUpService;
|
||||
let UserRepository: Repository<UserEntity>;
|
||||
let WorkspaceRepository: Repository<WorkspaceEntity>;
|
||||
let workspaceInvitationService: WorkspaceInvitationService;
|
||||
let userWorkspaceService: UserWorkspaceService;
|
||||
let twentyConfigService: TwentyConfigService;
|
||||
let subdomainManagerService: SubdomainManagerService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
SignInUpService,
|
||||
{
|
||||
provide: getRepositoryToken(UserEntity),
|
||||
useValue: {
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(WorkspaceEntity),
|
||||
useValue: {
|
||||
save: jest.fn(),
|
||||
create: jest.fn(),
|
||||
get: jest.fn(),
|
||||
count: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: FileUploadService,
|
||||
useValue: {
|
||||
uploadImage: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceInvitationService,
|
||||
useValue: {
|
||||
validatePersonalInvitation: jest.fn(),
|
||||
invalidateWorkspaceInvitation: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: UserWorkspaceService,
|
||||
useValue: {
|
||||
addUserToWorkspaceIfUserNotInWorkspace: jest.fn(),
|
||||
checkUserWorkspaceExists: jest.fn(),
|
||||
create: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: OnboardingService,
|
||||
useValue: {
|
||||
setOnboardingConnectAccountPending: jest.fn(),
|
||||
setOnboardingInviteTeamPending: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: HttpService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: LoginTokenService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: TwentyConfigService,
|
||||
useValue: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
markEmailAsVerified: jest.fn().mockReturnValue({
|
||||
id: 'test-user-id',
|
||||
email: 'test@test.com',
|
||||
isEmailVerified: true,
|
||||
} as UserEntity),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: SubdomainManagerService,
|
||||
useValue: {
|
||||
generateSubdomain: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: UserRoleService,
|
||||
useValue: {
|
||||
assignRoleToUserWorkspace: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: FeatureFlagService,
|
||||
useValue: {
|
||||
isFeatureEnabled: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceEventEmitter,
|
||||
useValue: {
|
||||
emitCustomBatchEvent: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: MetricsService,
|
||||
useValue: {
|
||||
incrementCounter: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<SignInUpService>(SignInUpService);
|
||||
UserRepository = module.get(getRepositoryToken(UserEntity));
|
||||
WorkspaceRepository = module.get(getRepositoryToken(WorkspaceEntity));
|
||||
workspaceInvitationService = module.get<WorkspaceInvitationService>(
|
||||
WorkspaceInvitationService,
|
||||
);
|
||||
userWorkspaceService =
|
||||
module.get<UserWorkspaceService>(UserWorkspaceService);
|
||||
twentyConfigService = module.get<TwentyConfigService>(TwentyConfigService);
|
||||
subdomainManagerService = module.get<SubdomainManagerService>(
|
||||
SubdomainManagerService,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle signInUp with valid personal invitation', async () => {
|
||||
const params: SignInUpBaseParams &
|
||||
ExistingUserOrPartialUserWithPicture &
|
||||
AuthProviderWithPasswordType = {
|
||||
invitation: { value: 'invitationToken' } as AppTokenEntity,
|
||||
workspace: {
|
||||
id: 'workspaceId',
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
} as WorkspaceEntity,
|
||||
authParams: {
|
||||
provider: AuthProviderEnum.Password,
|
||||
password: 'validPassword',
|
||||
},
|
||||
userData: {
|
||||
type: 'existingUser',
|
||||
existingUser: { email: 'test@example.com' } as UserEntity,
|
||||
},
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(workspaceInvitationService, 'validatePersonalInvitation')
|
||||
.mockResolvedValue({
|
||||
isValid: true,
|
||||
workspace: params.workspace as WorkspaceEntity,
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(workspaceInvitationService, 'invalidateWorkspaceInvitation')
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
jest
|
||||
.spyOn(userWorkspaceService, 'addUserToWorkspaceIfUserNotInWorkspace')
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.signInUp(params);
|
||||
|
||||
expect(result.workspace).toEqual(params.workspace);
|
||||
expect(result.user).toBeDefined();
|
||||
expect(
|
||||
workspaceInvitationService.validatePersonalInvitation,
|
||||
).toHaveBeenCalledWith({
|
||||
workspacePersonalInviteToken: 'invitationToken',
|
||||
email: 'test@example.com',
|
||||
});
|
||||
expect(
|
||||
workspaceInvitationService.invalidateWorkspaceInvitation,
|
||||
).toHaveBeenCalledWith(
|
||||
(params.workspace as WorkspaceEntity).id,
|
||||
'test@example.com',
|
||||
);
|
||||
expect(
|
||||
userWorkspaceService.addUserToWorkspaceIfUserNotInWorkspace,
|
||||
).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle signInUp on existing workspace without invitation', async () => {
|
||||
const params: SignInUpBaseParams &
|
||||
ExistingUserOrPartialUserWithPicture &
|
||||
AuthProviderWithPasswordType = {
|
||||
workspace: {
|
||||
id: 'workspaceId',
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
} as WorkspaceEntity,
|
||||
authParams: {
|
||||
provider: AuthProviderEnum.Password,
|
||||
password: 'validPassword',
|
||||
},
|
||||
userData: {
|
||||
type: 'existingUser',
|
||||
existingUser: { email: 'test@example.com' } as UserEntity,
|
||||
},
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(userWorkspaceService, 'addUserToWorkspaceIfUserNotInWorkspace')
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.signInUp(params);
|
||||
|
||||
expect(result.workspace).toEqual(params.workspace);
|
||||
expect(result.user).toBeDefined();
|
||||
expect(
|
||||
userWorkspaceService.addUserToWorkspaceIfUserNotInWorkspace,
|
||||
).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle signUp on new workspace for a new user', async () => {
|
||||
const params: SignInUpBaseParams &
|
||||
ExistingUserOrPartialUserWithPicture &
|
||||
AuthProviderWithPasswordType = {
|
||||
authParams: {
|
||||
provider: AuthProviderEnum.Password,
|
||||
password: 'validPassword',
|
||||
},
|
||||
userData: {
|
||||
type: 'newUserWithPicture',
|
||||
newUserWithPicture: {
|
||||
email: 'newuser@example.com',
|
||||
picture: 'pictureUrl',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
jest.spyOn(twentyConfigService, 'get').mockReturnValue(false);
|
||||
jest.spyOn(WorkspaceRepository, 'count').mockResolvedValue(0);
|
||||
jest
|
||||
.spyOn(WorkspaceRepository, 'create')
|
||||
.mockReturnValue({} as WorkspaceEntity);
|
||||
jest.spyOn(WorkspaceRepository, 'save').mockResolvedValue({
|
||||
id: 'newWorkspaceId',
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
} as WorkspaceEntity);
|
||||
jest.spyOn(UserRepository, 'create').mockReturnValue({} as UserEntity);
|
||||
jest
|
||||
.spyOn(subdomainManagerService, 'generateSubdomain')
|
||||
.mockResolvedValue('a-subdomain');
|
||||
jest
|
||||
.spyOn(UserRepository, 'save')
|
||||
|
||||
.mockResolvedValue({ id: 'newUserId' } as UserEntity);
|
||||
jest
|
||||
.spyOn(userWorkspaceService, 'create')
|
||||
.mockResolvedValue({} as UserWorkspaceEntity);
|
||||
|
||||
const result = await service.signInUp(params);
|
||||
|
||||
expect(result.workspace).toBeDefined();
|
||||
expect(result.user).toBeDefined();
|
||||
expect(WorkspaceRepository.create).toHaveBeenCalled();
|
||||
expect(WorkspaceRepository.save).toHaveBeenCalled();
|
||||
expect(UserRepository.create).toHaveBeenCalled();
|
||||
expect(UserRepository.save).toHaveBeenCalled();
|
||||
expect(userWorkspaceService.create).toHaveBeenCalledWith({
|
||||
workspaceId: 'newWorkspaceId',
|
||||
userId: 'newUserId',
|
||||
isExistingUser: false,
|
||||
pictureUrl: 'pictureUrl',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle signIn on workspace in pending state', async () => {
|
||||
const params: SignInUpBaseParams &
|
||||
ExistingUserOrPartialUserWithPicture &
|
||||
AuthProviderWithPasswordType = {
|
||||
workspace: {
|
||||
id: 'workspaceId',
|
||||
activationStatus: WorkspaceActivationStatus.PENDING_CREATION,
|
||||
} as WorkspaceEntity,
|
||||
authParams: {
|
||||
provider: AuthProviderEnum.Password,
|
||||
password: 'validPassword',
|
||||
},
|
||||
userData: {
|
||||
type: 'existingUser',
|
||||
existingUser: { email: 'test@example.com' } as UserEntity,
|
||||
},
|
||||
};
|
||||
|
||||
jest.spyOn(twentyConfigService, 'get').mockReturnValue(false);
|
||||
jest
|
||||
.spyOn(userWorkspaceService, 'addUserToWorkspaceIfUserNotInWorkspace')
|
||||
.mockResolvedValue(undefined);
|
||||
jest
|
||||
.spyOn(userWorkspaceService, 'checkUserWorkspaceExists')
|
||||
.mockResolvedValue({} as UserWorkspaceEntity);
|
||||
|
||||
const result = await service.signInUp(params);
|
||||
|
||||
expect(result.workspace).toEqual(params.workspace);
|
||||
expect(result.user).toBeDefined();
|
||||
expect(
|
||||
userWorkspaceService.addUserToWorkspaceIfUserNotInWorkspace,
|
||||
).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw - handle signUp on workspace in pending state', async () => {
|
||||
const params: SignInUpBaseParams &
|
||||
ExistingUserOrPartialUserWithPicture &
|
||||
AuthProviderWithPasswordType = {
|
||||
workspace: {
|
||||
id: 'workspaceId',
|
||||
activationStatus: WorkspaceActivationStatus.PENDING_CREATION,
|
||||
} as WorkspaceEntity,
|
||||
authParams: {
|
||||
provider: AuthProviderEnum.Password,
|
||||
password: 'validPassword',
|
||||
},
|
||||
userData: {
|
||||
type: 'existingUser',
|
||||
existingUser: { email: 'test@example.com' } as UserEntity,
|
||||
},
|
||||
};
|
||||
|
||||
jest.spyOn(twentyConfigService, 'get').mockReturnValue(false);
|
||||
jest
|
||||
.spyOn(userWorkspaceService, 'checkUserWorkspaceExists')
|
||||
.mockResolvedValue(null);
|
||||
|
||||
await expect(() => service.signInUp(params)).rejects.toThrow(
|
||||
new AuthException(
|
||||
'User is not part of the workspace',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle signup for existing user on new workspace', async () => {
|
||||
const params: SignInUpBaseParams &
|
||||
ExistingUserOrPartialUserWithPicture &
|
||||
AuthProviderWithPasswordType = {
|
||||
workspace: null,
|
||||
authParams: {
|
||||
provider: AuthProviderEnum.Password,
|
||||
password: 'validPassword',
|
||||
},
|
||||
userData: {
|
||||
type: 'existingUser',
|
||||
existingUser: { email: 'existinguser@example.com' } as UserEntity,
|
||||
},
|
||||
};
|
||||
|
||||
jest.spyOn(twentyConfigService, 'get').mockReturnValue(false);
|
||||
jest.spyOn(WorkspaceRepository, 'count').mockResolvedValue(0);
|
||||
jest
|
||||
.spyOn(WorkspaceRepository, 'create')
|
||||
.mockReturnValue({} as WorkspaceEntity);
|
||||
jest.spyOn(WorkspaceRepository, 'save').mockResolvedValue({
|
||||
id: 'newWorkspaceId',
|
||||
activationStatus: WorkspaceActivationStatus.PENDING_CREATION,
|
||||
} as WorkspaceEntity);
|
||||
jest.spyOn(userWorkspaceService, 'create').mockResolvedValue({} as any);
|
||||
|
||||
const result = await service.signInUp(params);
|
||||
|
||||
expect(result.workspace).toBeDefined();
|
||||
expect(result.user).toBeDefined();
|
||||
expect(WorkspaceRepository.create).toHaveBeenCalled();
|
||||
expect(WorkspaceRepository.save).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+114
-46
@@ -1,15 +1,16 @@
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { TWENTY_ICONS_BASE_URL } from 'twenty-shared/constants';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { Repository } from 'typeorm';
|
||||
import { type DataSource, type QueryRunner, Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { USER_SIGNUP_EVENT_NAME } from 'src/engine/api/graphql/workspace-query-runner/constants/user-signup-event-name.constants';
|
||||
import { type AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
@@ -38,6 +39,7 @@ import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-in
|
||||
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||
import { computeWorkspaceCustomCreateApplicationInput } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/compute-workspace-custom-create-application-input';
|
||||
import { getDomainNameByEmail } from 'src/utils/get-domain-name-by-email';
|
||||
import { isWorkEmail } from 'src/utils/is-work-email';
|
||||
|
||||
@@ -58,6 +60,9 @@ export class SignInUpService {
|
||||
private readonly subdomainManagerService: SubdomainManagerService,
|
||||
private readonly userService: UserService,
|
||||
private readonly metricsService: MetricsService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
@InjectDataSource()
|
||||
private readonly dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
async computePartialUserFromUserPayload(
|
||||
@@ -263,7 +268,10 @@ export class SignInUpService {
|
||||
canImpersonate: false,
|
||||
});
|
||||
|
||||
await this.activateOnboardingForUser(user, params.workspace);
|
||||
await this.activateOnboardingForUser({
|
||||
user,
|
||||
workspace: params.workspace,
|
||||
});
|
||||
|
||||
await this.userWorkspaceService.addUserToWorkspaceIfUserNotInWorkspace(
|
||||
user,
|
||||
@@ -289,21 +297,33 @@ export class SignInUpService {
|
||||
}
|
||||
|
||||
private async activateOnboardingForUser(
|
||||
user: UserEntity,
|
||||
workspace: WorkspaceEntity,
|
||||
{
|
||||
user,
|
||||
workspace,
|
||||
}: {
|
||||
user: UserEntity;
|
||||
workspace: WorkspaceEntity;
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
await this.onboardingService.setOnboardingConnectAccountPending({
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
value: true,
|
||||
});
|
||||
|
||||
if (user.firstName === '' && user.lastName === '') {
|
||||
await this.onboardingService.setOnboardingCreateProfilePending({
|
||||
await this.onboardingService.setOnboardingConnectAccountPending(
|
||||
{
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
value: true,
|
||||
});
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
if (user.firstName === '' && user.lastName === '') {
|
||||
await this.onboardingService.setOnboardingCreateProfilePending(
|
||||
{
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
value: true,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,6 +336,7 @@ export class SignInUpService {
|
||||
canImpersonate: boolean;
|
||||
canAccessFullAdminPanel: boolean;
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
const userCreated = this.userRepository.create({
|
||||
...newUserWithPicture,
|
||||
@@ -323,7 +344,9 @@ export class SignInUpService {
|
||||
canAccessFullAdminPanel,
|
||||
});
|
||||
|
||||
const savedUser = await this.userRepository.save(userCreated);
|
||||
const savedUser = queryRunner
|
||||
? await queryRunner.manager.save(UserEntity, userCreated)
|
||||
: await this.userRepository.save(userCreated);
|
||||
|
||||
const serverUrl = this.twentyConfigService.get('SERVER_URL');
|
||||
|
||||
@@ -435,43 +458,88 @@ export class SignInUpService {
|
||||
const logo =
|
||||
isWorkEmailFound && (await isLogoUrlValid()) ? logoUrl : undefined;
|
||||
|
||||
const workspaceToCreate = this.workspaceRepository.create({
|
||||
subdomain: await this.subdomainManagerService.generateSubdomain(
|
||||
isWorkEmailFound ? { userEmail: email } : {},
|
||||
),
|
||||
displayName: '',
|
||||
inviteHash: v4(),
|
||||
activationStatus: WorkspaceActivationStatus.PENDING_CREATION,
|
||||
logo,
|
||||
});
|
||||
const workspaceId = v4();
|
||||
const workspaceCustomApplicationCreateInput =
|
||||
computeWorkspaceCustomCreateApplicationInput({
|
||||
workspace: {
|
||||
id: workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
const workspace = await this.workspaceRepository.save(workspaceToCreate);
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
|
||||
const isExistingUser = userData.type === 'existingUser';
|
||||
const user = isExistingUser
|
||||
? userData.existingUser
|
||||
: await this.saveNewUser(userData.newUserWithPicture, {
|
||||
canImpersonate,
|
||||
canAccessFullAdminPanel,
|
||||
});
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
await this.userWorkspaceService.create({
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
isExistingUser,
|
||||
pictureUrl: isExistingUser
|
||||
? undefined
|
||||
: userData.newUserWithPicture.picture,
|
||||
});
|
||||
try {
|
||||
const workspaceCustomApplication = await this.applicationService.create(
|
||||
{
|
||||
...workspaceCustomApplicationCreateInput,
|
||||
serverlessFunctionLayerId: null,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await this.activateOnboardingForUser(user, workspace);
|
||||
const workspaceToCreate = this.workspaceRepository.create({
|
||||
id: workspaceId,
|
||||
subdomain: await this.subdomainManagerService.generateSubdomain(
|
||||
isWorkEmailFound ? { userEmail: email } : {},
|
||||
),
|
||||
workspaceCustomApplicationId: workspaceCustomApplication.id,
|
||||
displayName: '',
|
||||
inviteHash: v4(),
|
||||
activationStatus: WorkspaceActivationStatus.PENDING_CREATION,
|
||||
logo,
|
||||
});
|
||||
|
||||
await this.onboardingService.setOnboardingInviteTeamPending({
|
||||
workspaceId: workspace.id,
|
||||
value: true,
|
||||
});
|
||||
const workspace = await queryRunner.manager.save(
|
||||
WorkspaceEntity,
|
||||
workspaceToCreate,
|
||||
);
|
||||
|
||||
return { user, workspace };
|
||||
const isExistingUser = userData.type === 'existingUser';
|
||||
const user = isExistingUser
|
||||
? userData.existingUser
|
||||
: await this.saveNewUser(
|
||||
userData.newUserWithPicture,
|
||||
{
|
||||
canImpersonate,
|
||||
canAccessFullAdminPanel,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await this.userWorkspaceService.create(
|
||||
{
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
isExistingUser,
|
||||
pictureUrl: isExistingUser
|
||||
? undefined
|
||||
: userData.newUserWithPicture.picture,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await this.activateOnboardingForUser({ user, workspace }, queryRunner);
|
||||
|
||||
await this.onboardingService.setOnboardingInviteTeamPending(
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
value: true,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
return { user, workspace };
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
async signUpWithoutWorkspace(
|
||||
|
||||
+54
-31
@@ -1,6 +1,6 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import { type QueryRunner, IsNull, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
KeyValuePairEntity,
|
||||
@@ -50,19 +50,22 @@ export class KeyValuePairService<
|
||||
}));
|
||||
}
|
||||
|
||||
async set<K extends keyof KeyValueTypesMap>({
|
||||
userId,
|
||||
workspaceId,
|
||||
key,
|
||||
value,
|
||||
type,
|
||||
}: {
|
||||
userId?: string | null;
|
||||
workspaceId?: string | null;
|
||||
key: Extract<K, string>;
|
||||
value: KeyValueTypesMap[K];
|
||||
type: KeyValuePairType;
|
||||
}) {
|
||||
async set<K extends keyof KeyValueTypesMap>(
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
key,
|
||||
value,
|
||||
type,
|
||||
}: {
|
||||
userId?: string | null;
|
||||
workspaceId?: string | null;
|
||||
key: Extract<K, string>;
|
||||
value: KeyValueTypesMap[K];
|
||||
type: KeyValuePairType;
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
const upsertData = {
|
||||
userId,
|
||||
workspaceId,
|
||||
@@ -84,24 +87,36 @@ export class KeyValuePairService<
|
||||
? '"workspaceId" is NULL'
|
||||
: undefined;
|
||||
|
||||
await this.keyValuePairRepository.upsert(upsertData, {
|
||||
conflictPaths,
|
||||
indexPredicate,
|
||||
});
|
||||
if (queryRunner) {
|
||||
await queryRunner.manager
|
||||
.getRepository(KeyValuePairEntity)
|
||||
.upsert(upsertData, {
|
||||
conflictPaths,
|
||||
indexPredicate,
|
||||
});
|
||||
} else {
|
||||
await this.keyValuePairRepository.upsert(upsertData, {
|
||||
conflictPaths,
|
||||
indexPredicate,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async delete({
|
||||
userId,
|
||||
workspaceId,
|
||||
type,
|
||||
key,
|
||||
}: {
|
||||
userId?: string | null;
|
||||
workspaceId?: string | null;
|
||||
type: KeyValuePairType;
|
||||
key: Extract<keyof KeyValueTypesMap, string>;
|
||||
}) {
|
||||
await this.keyValuePairRepository.delete({
|
||||
async delete(
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
type,
|
||||
key,
|
||||
}: {
|
||||
userId?: string | null;
|
||||
workspaceId?: string | null;
|
||||
type: KeyValuePairType;
|
||||
key: Extract<keyof KeyValueTypesMap, string>;
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
const deleteConditions = {
|
||||
...(userId === undefined
|
||||
? {}
|
||||
: userId === null
|
||||
@@ -114,6 +129,14 @@ export class KeyValuePairService<
|
||||
: { workspaceId }),
|
||||
type,
|
||||
key,
|
||||
});
|
||||
};
|
||||
|
||||
if (queryRunner) {
|
||||
await queryRunner.manager
|
||||
.getRepository(KeyValuePairEntity)
|
||||
.delete(deleteConditions);
|
||||
} else {
|
||||
await this.keyValuePairRepository.delete(deleteConditions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
||||
import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
|
||||
@@ -107,81 +108,108 @@ export class OnboardingService {
|
||||
return OnboardingStatus.COMPLETED;
|
||||
}
|
||||
|
||||
async setOnboardingConnectAccountPending({
|
||||
userId,
|
||||
workspaceId,
|
||||
value,
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
value: boolean;
|
||||
}) {
|
||||
async setOnboardingConnectAccountPending(
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
value,
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
value: boolean;
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
if (!value) {
|
||||
await this.userVarsService.delete({
|
||||
userId,
|
||||
workspaceId,
|
||||
key: OnboardingStepKeys.ONBOARDING_CONNECT_ACCOUNT_PENDING,
|
||||
});
|
||||
await this.userVarsService.delete(
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
key: OnboardingStepKeys.ONBOARDING_CONNECT_ACCOUNT_PENDING,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.userVarsService.set({
|
||||
userId,
|
||||
workspaceId: workspaceId,
|
||||
key: OnboardingStepKeys.ONBOARDING_CONNECT_ACCOUNT_PENDING,
|
||||
value: true,
|
||||
});
|
||||
await this.userVarsService.set(
|
||||
{
|
||||
userId,
|
||||
workspaceId: workspaceId,
|
||||
key: OnboardingStepKeys.ONBOARDING_CONNECT_ACCOUNT_PENDING,
|
||||
value: true,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
async setOnboardingInviteTeamPending({
|
||||
workspaceId,
|
||||
value,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
value: boolean;
|
||||
}) {
|
||||
async setOnboardingInviteTeamPending(
|
||||
{
|
||||
workspaceId,
|
||||
value,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
value: boolean;
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
if (!value) {
|
||||
await this.userVarsService.delete({
|
||||
await this.userVarsService.delete(
|
||||
{
|
||||
workspaceId,
|
||||
key: OnboardingStepKeys.ONBOARDING_INVITE_TEAM_PENDING,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.userVarsService.set(
|
||||
{
|
||||
workspaceId,
|
||||
key: OnboardingStepKeys.ONBOARDING_INVITE_TEAM_PENDING,
|
||||
});
|
||||
value: true,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
async setOnboardingCreateProfilePending(
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
value,
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
value: boolean;
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
if (!value) {
|
||||
await this.userVarsService.delete(
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
key: OnboardingStepKeys.ONBOARDING_CREATE_PROFILE_PENDING,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.userVarsService.set({
|
||||
workspaceId,
|
||||
key: OnboardingStepKeys.ONBOARDING_INVITE_TEAM_PENDING,
|
||||
value: true,
|
||||
});
|
||||
}
|
||||
|
||||
async setOnboardingCreateProfilePending({
|
||||
userId,
|
||||
workspaceId,
|
||||
value,
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
value: boolean;
|
||||
}) {
|
||||
if (!value) {
|
||||
await this.userVarsService.delete({
|
||||
await this.userVarsService.set(
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
key: OnboardingStepKeys.ONBOARDING_CREATE_PROFILE_PENDING,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.userVarsService.set({
|
||||
userId,
|
||||
workspaceId,
|
||||
key: OnboardingStepKeys.ONBOARDING_CREATE_PROFILE_PENDING,
|
||||
value: true,
|
||||
});
|
||||
value: true,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
async setOnboardingBookOnboardingPending({
|
||||
|
||||
+18
-11
@@ -420,23 +420,30 @@ describe('UserWorkspaceService', () => {
|
||||
user.id,
|
||||
workspace.id,
|
||||
);
|
||||
expect(service.create).toHaveBeenCalledWith({
|
||||
workspaceId: workspace.id,
|
||||
userId: user.id,
|
||||
isExistingUser: true,
|
||||
});
|
||||
expect(service.create).toHaveBeenCalled();
|
||||
expect(service.create).toHaveBeenCalledWith(
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
userId: user.id,
|
||||
isExistingUser: true,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
expect(service.createWorkspaceMember).toHaveBeenCalledWith(
|
||||
workspace.id,
|
||||
user,
|
||||
);
|
||||
expect(userRoleService.assignRoleToUserWorkspace).toHaveBeenCalledWith({
|
||||
workspaceId: workspace.id,
|
||||
userWorkspaceId: userWorkspace.id,
|
||||
roleId: workspace.defaultRoleId,
|
||||
});
|
||||
expect(userRoleService.assignRoleToUserWorkspace).toHaveBeenCalledWith(
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
userWorkspaceId: userWorkspace.id,
|
||||
roleId: workspace.defaultRoleId,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
expect(
|
||||
workspaceInvitationService.invalidateWorkspaceInvitation,
|
||||
).toHaveBeenCalledWith(workspace.id, user.email);
|
||||
).toHaveBeenCalledWith(workspace.id, user.email, undefined);
|
||||
});
|
||||
|
||||
it('should not add user to workspace if already in workspace', async () => {
|
||||
|
||||
+36
-23
@@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Not, Repository } from 'typeorm';
|
||||
import { type QueryRunner, IsNull, Not, Repository } from 'typeorm';
|
||||
|
||||
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
|
||||
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||
@@ -57,17 +57,20 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
|
||||
super(userWorkspaceRepository);
|
||||
}
|
||||
|
||||
async create({
|
||||
userId,
|
||||
workspaceId,
|
||||
isExistingUser,
|
||||
pictureUrl,
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
isExistingUser: boolean;
|
||||
pictureUrl?: string;
|
||||
}): Promise<UserWorkspaceEntity> {
|
||||
async create(
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
isExistingUser,
|
||||
pictureUrl,
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
isExistingUser: boolean;
|
||||
pictureUrl?: string;
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
): Promise<UserWorkspaceEntity> {
|
||||
const defaultAvatarUrl = await this.computeDefaultAvatarUrl(
|
||||
userId,
|
||||
workspaceId,
|
||||
@@ -81,7 +84,9 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
|
||||
defaultAvatarUrl,
|
||||
});
|
||||
|
||||
return this.userWorkspaceRepository.save(userWorkspace);
|
||||
return queryRunner
|
||||
? queryRunner.manager.save(UserWorkspaceEntity, userWorkspace)
|
||||
: this.userWorkspaceRepository.save(userWorkspace);
|
||||
}
|
||||
|
||||
async createWorkspaceMember(workspaceId: string, user: UserEntity) {
|
||||
@@ -126,6 +131,7 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
|
||||
async addUserToWorkspaceIfUserNotInWorkspace(
|
||||
user: UserEntity,
|
||||
workspace: WorkspaceEntity,
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
let userWorkspace = await this.checkUserWorkspaceExists(
|
||||
user.id,
|
||||
@@ -133,11 +139,14 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
|
||||
);
|
||||
|
||||
if (!userWorkspace) {
|
||||
userWorkspace = await this.create({
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
isExistingUser: true,
|
||||
});
|
||||
userWorkspace = await this.create(
|
||||
{
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
isExistingUser: true,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await this.createWorkspaceMember(workspace.id, user);
|
||||
|
||||
@@ -150,15 +159,19 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
|
||||
);
|
||||
}
|
||||
|
||||
await this.userRoleService.assignRoleToUserWorkspace({
|
||||
workspaceId: workspace.id,
|
||||
userWorkspaceId: userWorkspace.id,
|
||||
roleId: defaultRoleId,
|
||||
});
|
||||
await this.userRoleService.assignRoleToUserWorkspace(
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
userWorkspaceId: userWorkspace.id,
|
||||
roleId: defaultRoleId,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await this.workspaceInvitationService.invalidateWorkspaceInvitation(
|
||||
workspace.id,
|
||||
user.email,
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { type Repository, type UpdateResult } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { type UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
@@ -43,6 +44,7 @@ describe('UserService', () => {
|
||||
findOne: jest.fn(),
|
||||
save: jest.fn(),
|
||||
softDelete: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -67,6 +69,10 @@ describe('UserService', () => {
|
||||
deleteUserWorkspace: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ApplicationService,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { msg } from '@lingui/core/macro';
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
|
||||
import { IsNull, Not, Repository } from 'typeorm';
|
||||
import { type QueryRunner, IsNull, Not, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
AuthException,
|
||||
@@ -275,11 +275,13 @@ export class UserService extends TypeOrmQueryService<UserEntity> {
|
||||
return user;
|
||||
}
|
||||
|
||||
async markEmailAsVerified(userId: string) {
|
||||
async markEmailAsVerified(userId: string, queryRunner?: QueryRunner) {
|
||||
const user = await this.findUserByIdOrThrow(userId);
|
||||
|
||||
user.isEmailVerified = true;
|
||||
|
||||
return await this.userRepository.save(user);
|
||||
return queryRunner
|
||||
? await queryRunner.manager.save(UserEntity, user)
|
||||
: await this.userRepository.save(user);
|
||||
}
|
||||
}
|
||||
|
||||
+46
-32
@@ -1,5 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
import { KeyValuePairType } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
||||
import { KeyValuePairService } from 'src/engine/core-modules/key-value-pair/key-value-pair.service';
|
||||
import { mergeUserVars } from 'src/engine/core-modules/user/user-vars/utils/merge-user-vars.util';
|
||||
@@ -126,40 +128,52 @@ export class UserVarsService<
|
||||
return mergeUserVars<Extract<keyof KeyValueTypesMap, string>>(result);
|
||||
}
|
||||
|
||||
set<K extends keyof KeyValueTypesMap>({
|
||||
userId,
|
||||
workspaceId,
|
||||
key,
|
||||
value,
|
||||
}: {
|
||||
userId?: string;
|
||||
workspaceId?: string;
|
||||
key: Extract<K, string>;
|
||||
value: KeyValueTypesMap[K];
|
||||
}) {
|
||||
return this.keyValuePairService.set({
|
||||
userId,
|
||||
workspaceId,
|
||||
key: key,
|
||||
value,
|
||||
type: KeyValuePairType.USER_VARIABLE,
|
||||
});
|
||||
}
|
||||
|
||||
async delete({
|
||||
userId,
|
||||
workspaceId,
|
||||
key,
|
||||
}: {
|
||||
userId?: string;
|
||||
workspaceId?: string;
|
||||
key: Extract<keyof KeyValueTypesMap, string>;
|
||||
}) {
|
||||
return this.keyValuePairService.delete({
|
||||
set<K extends keyof KeyValueTypesMap>(
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
key,
|
||||
type: KeyValuePairType.USER_VARIABLE,
|
||||
});
|
||||
value,
|
||||
}: {
|
||||
userId?: string;
|
||||
workspaceId?: string;
|
||||
key: Extract<K, string>;
|
||||
value: KeyValueTypesMap[K];
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
return this.keyValuePairService.set(
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
key: key,
|
||||
value,
|
||||
type: KeyValuePairType.USER_VARIABLE,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
async delete(
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
key,
|
||||
}: {
|
||||
userId?: string;
|
||||
workspaceId?: string;
|
||||
key: Extract<keyof KeyValueTypesMap, string>;
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
return this.keyValuePairService.delete(
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
key,
|
||||
type: KeyValuePairType.USER_VARIABLE,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+16
-4
@@ -9,8 +9,8 @@ import { addMilliseconds } from 'date-fns';
|
||||
import ms from 'ms';
|
||||
import { SendInviteLinkEmail } from 'twenty-emails';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { getAppPath } from 'twenty-shared/utils';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import { getAppPath, isDefined } from 'twenty-shared/utils';
|
||||
import { type QueryRunner, IsNull, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
AppTokenEntity,
|
||||
@@ -203,10 +203,22 @@ export class WorkspaceInvitationService {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
async invalidateWorkspaceInvitation(workspaceId: string, email: string) {
|
||||
async invalidateWorkspaceInvitation(
|
||||
workspaceId: string,
|
||||
email: string,
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
const appToken = await this.getOneWorkspaceInvitation(workspaceId, email);
|
||||
|
||||
if (appToken) {
|
||||
if (!isDefined(appToken)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (queryRunner) {
|
||||
await queryRunner.manager
|
||||
.getRepository(AppTokenEntity)
|
||||
.delete(appToken.id);
|
||||
} else {
|
||||
await this.appTokenRepository.delete(appToken.id);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -247,7 +247,7 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
|
||||
);
|
||||
|
||||
await this.workspaceManagerService.init({
|
||||
workspaceId: workspace.id,
|
||||
workspace,
|
||||
userId: user.id,
|
||||
});
|
||||
await this.userWorkspaceService.createWorkspaceMember(workspace.id, user);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
import { Application } from 'cloudflare/resources/zero-trust/access/applications/applications';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import {
|
||||
Check,
|
||||
@@ -9,6 +10,8 @@ import {
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
Relation,
|
||||
@@ -19,6 +22,7 @@ import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/
|
||||
import { ModelId } from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||
import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
import { AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { ApprovedAccessDomainEntity } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.entity';
|
||||
import { EmailingDomainEntity } from 'src/engine/core-modules/emailing-domain/emailing-domain.entity';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
@@ -272,4 +276,22 @@ export class WorkspaceEntity {
|
||||
@Field(() => String, { nullable: false })
|
||||
@Column({ type: 'varchar', nullable: false, default: 'auto' })
|
||||
routerModel: ModelId;
|
||||
|
||||
// TODO prastoin
|
||||
// Temporarily setting as nullable for retro compatibility, not udpating TypeScript types
|
||||
@Field(() => String, { nullable: true })
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
workspaceCustomApplicationId: string;
|
||||
|
||||
@ManyToOne(() => ApplicationEntity, {
|
||||
onDelete: 'RESTRICT',
|
||||
nullable: false,
|
||||
})
|
||||
@JoinColumn({ name: 'workspaceCustomApplicationId' })
|
||||
workspaceCustomApplication: Relation<ApplicationEntity>;
|
||||
|
||||
@OneToMany(() => ApplicationEntity, (application) => application.workspace, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
applications: Relation<Application[]>;
|
||||
}
|
||||
|
||||
@@ -292,6 +292,11 @@ export class WorkspaceResolver {
|
||||
);
|
||||
}
|
||||
|
||||
@ResolveField(() => String)
|
||||
workspaceCustomApplicationId(@Parent() workspace: WorkspaceEntity) {
|
||||
return workspace.workspaceCustomApplicationId;
|
||||
}
|
||||
|
||||
@ResolveField(() => Boolean)
|
||||
isMicrosoftAuthEnabled(@Parent() workspace: WorkspaceEntity) {
|
||||
return (
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
|
||||
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
@@ -17,7 +17,7 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([RouteTriggerEntity]),
|
||||
AuthModule,
|
||||
TokenModule,
|
||||
WorkspaceDomainsModule,
|
||||
ServerlessFunctionModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In, Not, Repository } from 'typeorm';
|
||||
import { type QueryRunner, In, Not, Repository } from 'typeorm';
|
||||
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import {
|
||||
@@ -29,15 +29,18 @@ export class UserRoleService {
|
||||
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
|
||||
) {}
|
||||
|
||||
public async assignRoleToUserWorkspace({
|
||||
workspaceId,
|
||||
userWorkspaceId,
|
||||
roleId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
userWorkspaceId: string;
|
||||
roleId: string;
|
||||
}): Promise<void> {
|
||||
public async assignRoleToUserWorkspace(
|
||||
{
|
||||
workspaceId,
|
||||
userWorkspaceId,
|
||||
roleId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
userWorkspaceId: string;
|
||||
roleId: string;
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
): Promise<void> {
|
||||
const validationResult = await this.validateAssignRoleInput({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
@@ -48,13 +51,17 @@ export class UserRoleService {
|
||||
return;
|
||||
}
|
||||
|
||||
const newRoleTarget = await this.roleTargetsRepository.save({
|
||||
const roleTargetsRepo = queryRunner
|
||||
? queryRunner.manager.getRepository(RoleTargetsEntity)
|
||||
: this.roleTargetsRepository;
|
||||
|
||||
const newRoleTarget = await roleTargetsRepo.save({
|
||||
roleId,
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
await this.roleTargetsRepository.delete({
|
||||
await roleTargetsRepo.delete({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
id: Not(newRoleTarget.id),
|
||||
|
||||
+5
@@ -3,6 +3,7 @@ import { getDataSourceToken, getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { type DataSource, type Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@@ -104,6 +105,10 @@ describe('WorkspaceManagerService', () => {
|
||||
provide: RoleService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: ApplicationService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: UserRoleService,
|
||||
useValue: {},
|
||||
|
||||
+13
-7
@@ -1,13 +1,19 @@
|
||||
import { type DataSource } from 'typeorm';
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
const tableName = 'billingCustomer';
|
||||
|
||||
export const seedBillingCustomers = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
) => {
|
||||
await dataSource
|
||||
type SeedBillingCustomersArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const seedBillingCustomers = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: SeedBillingCustomersArgs) => {
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${tableName}`, ['workspaceId', 'stripeCustomerId'])
|
||||
|
||||
+13
-7
@@ -1,13 +1,19 @@
|
||||
import { type DataSource } from 'typeorm';
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
const tableName = 'billingSubscription';
|
||||
|
||||
export const seedBillingSubscriptions = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
) => {
|
||||
await dataSource
|
||||
type SeedBillingSubscriptionsArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const seedBillingSubscriptions = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: SeedBillingSubscriptionsArgs) => {
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${tableName}`, [
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
export const WORKSPACE_FIELDS_TO_SEED = [
|
||||
'id',
|
||||
'displayName',
|
||||
'subdomain',
|
||||
'inviteHash',
|
||||
'logo',
|
||||
'activationStatus',
|
||||
'isTwoFactorAuthenticationEnforced',
|
||||
'version',
|
||||
'workspaceCustomApplicationId',
|
||||
] as const satisfies (keyof WorkspaceEntity)[];
|
||||
|
||||
export type CreateWorkspaceInput = Pick<
|
||||
WorkspaceEntity,
|
||||
(typeof WORKSPACE_FIELDS_TO_SEED)[number]
|
||||
>;
|
||||
|
||||
export const SEED_APPLE_WORKSPACE_ID = '20202020-1c25-4d02-bf25-6aeccf7ea419';
|
||||
export const SEED_YCOMBINATOR_WORKSPACE_ID =
|
||||
'3b8e6458-5fc1-4e63-8563-008ccddaa6db';
|
||||
|
||||
export type SeededWorkspacesIds =
|
||||
| typeof SEED_APPLE_WORKSPACE_ID
|
||||
| typeof SEED_YCOMBINATOR_WORKSPACE_ID;
|
||||
|
||||
export const SEEDER_CREATE_WORKSPACE_INPUT = {
|
||||
[SEED_APPLE_WORKSPACE_ID]: {
|
||||
id: SEED_APPLE_WORKSPACE_ID,
|
||||
displayName: 'Apple',
|
||||
subdomain: 'apple',
|
||||
inviteHash: 'apple.dev-invite-hash',
|
||||
logo: 'https://twentyhq.github.io/placeholder-images/workspaces/apple-logo.png',
|
||||
activationStatus: WorkspaceActivationStatus.PENDING_CREATION, // will be set to active after default role creation
|
||||
isTwoFactorAuthenticationEnforced: false,
|
||||
},
|
||||
[SEED_YCOMBINATOR_WORKSPACE_ID]: {
|
||||
id: SEED_YCOMBINATOR_WORKSPACE_ID,
|
||||
displayName: 'YCombinator',
|
||||
subdomain: 'yc',
|
||||
inviteHash: 'yc.dev-invite-hash',
|
||||
logo: 'https://twentyhq.github.io/placeholder-images/workspaces/ycombinator-logo.png',
|
||||
activationStatus: WorkspaceActivationStatus.PENDING_CREATION, // will be set to active after default role creation
|
||||
isTwoFactorAuthenticationEnforced: false,
|
||||
},
|
||||
} as const satisfies Record<
|
||||
SeededWorkspacesIds,
|
||||
Omit<CreateWorkspaceInput, 'version' | 'workspaceCustomApplicationId'>
|
||||
>;
|
||||
+4
-4
@@ -12,14 +12,14 @@ import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import {
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
SEED_YCOMBINATOR_WORKSPACE_ID,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
import {
|
||||
RANDOM_USER_WORKSPACE_IDS,
|
||||
USER_WORKSPACE_DATA_SEED_IDS,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-user-workspaces.util';
|
||||
import {
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
SEED_YCOMBINATOR_WORKSPACE_ID,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||
import { API_KEY_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/api-key-data-seeds.constant';
|
||||
import { ADMIN_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role';
|
||||
|
||||
|
||||
+53
-25
@@ -1,11 +1,11 @@
|
||||
import { type DataSource } from 'typeorm';
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
import { AgentChatMessageRole } from 'src/engine/metadata-modules/agent/agent-chat-message.entity';
|
||||
import { USER_WORKSPACE_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-user-workspaces.util';
|
||||
import {
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
SEED_YCOMBINATOR_WORKSPACE_ID,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
import { USER_WORKSPACE_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-user-workspaces.util';
|
||||
|
||||
const agentChatThreadTableName = 'agentChatThread';
|
||||
const agentChatMessageTableName = 'agentChatMessage';
|
||||
@@ -43,11 +43,17 @@ export const AGENT_CHAT_MESSAGE_PART_DATA_SEED_IDS = {
|
||||
YCOMBINATOR_MESSAGE_4_PART_1: '20202020-0000-4000-8000-000000000054',
|
||||
};
|
||||
|
||||
const seedChatThreads = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
) => {
|
||||
type SeedChatThreadsArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
const seedChatThreads = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: SeedChatThreadsArgs) => {
|
||||
let threadId: string;
|
||||
let userWorkspaceId: string;
|
||||
|
||||
@@ -65,7 +71,7 @@ const seedChatThreads = async (
|
||||
|
||||
const now = new Date();
|
||||
|
||||
await dataSource
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${agentChatThreadTableName}`, [
|
||||
@@ -88,12 +94,19 @@ const seedChatThreads = async (
|
||||
return threadId;
|
||||
};
|
||||
|
||||
const seedChatMessages = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
threadId: string,
|
||||
) => {
|
||||
type SeedChatMessagesArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
workspaceId: string;
|
||||
threadId: string;
|
||||
};
|
||||
|
||||
const seedChatMessages = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
threadId,
|
||||
}: SeedChatMessagesArgs) => {
|
||||
let messageIds: string[];
|
||||
let partIds: string[];
|
||||
let messages: Array<{
|
||||
@@ -274,7 +287,7 @@ const seedChatMessages = async (
|
||||
);
|
||||
}
|
||||
|
||||
await dataSource
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${agentChatMessageTableName}`, [
|
||||
@@ -287,7 +300,7 @@ const seedChatMessages = async (
|
||||
.values(messages)
|
||||
.execute();
|
||||
|
||||
await dataSource
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${agentChatMessagePartTableName}`, [
|
||||
@@ -303,12 +316,27 @@ const seedChatMessages = async (
|
||||
.execute();
|
||||
};
|
||||
|
||||
export const seedAgents = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
) => {
|
||||
const threadId = await seedChatThreads(dataSource, schemaName, workspaceId);
|
||||
|
||||
await seedChatMessages(dataSource, schemaName, workspaceId, threadId);
|
||||
type SeedAgentsArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const seedAgents = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: SeedAgentsArgs) => {
|
||||
const threadId = await seedChatThreads({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
await seedChatMessages({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
threadId,
|
||||
});
|
||||
};
|
||||
|
||||
+13
-7
@@ -1,15 +1,21 @@
|
||||
import { type DataSource } from 'typeorm';
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
import { API_KEY_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/api-key-data-seeds.constant';
|
||||
|
||||
const tableName = 'apiKey';
|
||||
|
||||
export const seedApiKeys = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
) => {
|
||||
await dataSource
|
||||
type SeedApiKeysArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const seedApiKeys = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: SeedApiKeysArgs) => {
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${tableName}`, [
|
||||
|
||||
+71
-18
@@ -1,18 +1,26 @@
|
||||
import { type DataSource } from 'typeorm';
|
||||
|
||||
import { type ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { seedBillingCustomers } from 'src/engine/workspace-manager/dev-seeder/core/billing/utils/seed-billing-customers.util';
|
||||
import { seedBillingSubscriptions } from 'src/engine/workspace-manager/dev-seeder/core/billing/utils/seed-billing-subscriptions.util';
|
||||
import {
|
||||
type SeededWorkspacesIds,
|
||||
SEEDER_CREATE_WORKSPACE_INPUT,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
import { seedAgents } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-agents.util';
|
||||
import { seedApiKeys } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-api-keys.util';
|
||||
import { seedFeatureFlags } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util';
|
||||
import { seedUserWorkspaces } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-user-workspaces.util';
|
||||
import { seedUsers } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-users.util';
|
||||
import { seedWorkspaces } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||
import { createWorkspace } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspace.util';
|
||||
import { computeWorkspaceCustomCreateApplicationInput } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/compute-workspace-custom-create-application-input';
|
||||
import { extractVersionMajorMinorPatch } from 'src/utils/version/extract-version-major-minor-patch';
|
||||
|
||||
type SeedCoreSchemaArgs = {
|
||||
dataSource: DataSource;
|
||||
workspaceId: string;
|
||||
workspaceId: SeededWorkspacesIds;
|
||||
appVersion: string | undefined;
|
||||
applicationService: ApplicationService;
|
||||
seedBilling?: boolean;
|
||||
seedFeatureFlags?: boolean;
|
||||
};
|
||||
@@ -21,30 +29,75 @@ export const seedCoreSchema = async ({
|
||||
appVersion,
|
||||
dataSource,
|
||||
workspaceId,
|
||||
applicationService,
|
||||
seedBilling = true,
|
||||
seedFeatureFlags: shouldSeedFeatureFlags = true,
|
||||
}: SeedCoreSchemaArgs) => {
|
||||
const schemaName = 'core';
|
||||
|
||||
await seedWorkspaces({
|
||||
dataSource,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
appVersion,
|
||||
});
|
||||
await seedUsers(dataSource, schemaName);
|
||||
await seedUserWorkspaces(dataSource, schemaName, workspaceId);
|
||||
const createWorkspaceStaticInput = SEEDER_CREATE_WORKSPACE_INPUT[workspaceId];
|
||||
const workspaceCustomApplicationCreateInput =
|
||||
computeWorkspaceCustomCreateApplicationInput({
|
||||
workspace: {
|
||||
id: workspaceId,
|
||||
displayName: createWorkspaceStaticInput.displayName,
|
||||
},
|
||||
});
|
||||
|
||||
await seedAgents(dataSource, schemaName, workspaceId);
|
||||
const version = extractVersionMajorMinorPatch(appVersion);
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
|
||||
await seedApiKeys(dataSource, schemaName, workspaceId);
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
if (shouldSeedFeatureFlags) {
|
||||
await seedFeatureFlags(dataSource, schemaName, workspaceId);
|
||||
}
|
||||
try {
|
||||
const customWorkspaceApplication = await applicationService.create(
|
||||
{
|
||||
...workspaceCustomApplicationCreateInput,
|
||||
serverlessFunctionLayerId: null,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
if (seedBilling) {
|
||||
await seedBillingCustomers(dataSource, schemaName, workspaceId);
|
||||
await seedBillingSubscriptions(dataSource, schemaName, workspaceId);
|
||||
await createWorkspace({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
createWorkspaceInput: {
|
||||
...createWorkspaceStaticInput,
|
||||
version,
|
||||
workspaceCustomApplicationId: customWorkspaceApplication.id,
|
||||
},
|
||||
});
|
||||
|
||||
await seedUsers({ queryRunner, schemaName });
|
||||
|
||||
await seedUserWorkspaces({ queryRunner, schemaName, workspaceId });
|
||||
|
||||
await applicationService.createTwentyStandardApplication(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await seedAgents({ queryRunner, schemaName, workspaceId });
|
||||
|
||||
await seedApiKeys({ queryRunner, schemaName, workspaceId });
|
||||
|
||||
if (shouldSeedFeatureFlags) {
|
||||
await seedFeatureFlags({ queryRunner, schemaName, workspaceId });
|
||||
}
|
||||
|
||||
if (seedBilling) {
|
||||
await seedBillingCustomers({ queryRunner, schemaName, workspaceId });
|
||||
await seedBillingSubscriptions({ queryRunner, schemaName, workspaceId });
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
};
|
||||
|
||||
+26
-14
@@ -1,16 +1,22 @@
|
||||
import { type DataSource } from 'typeorm';
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
|
||||
const tableName = 'featureFlag';
|
||||
|
||||
export const seedFeatureFlags = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
) => {
|
||||
await dataSource
|
||||
type SeedFeatureFlagsArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const seedFeatureFlags = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: SeedFeatureFlagsArgs) => {
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${tableName}`, ['key', 'workspaceId', 'value'])
|
||||
@@ -90,12 +96,18 @@ export const seedFeatureFlags = async (
|
||||
.execute();
|
||||
};
|
||||
|
||||
export const deleteFeatureFlags = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
) => {
|
||||
await dataSource
|
||||
type DeleteFeatureFlagsArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const deleteFeatureFlags = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: DeleteFeatureFlagsArgs) => {
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(`${schemaName}.${tableName}`)
|
||||
|
||||
+28
-16
@@ -1,12 +1,12 @@
|
||||
import { type DataSource } from 'typeorm';
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
import { type UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { generateRandomUsers } from 'src/engine/workspace-manager/dev-seeder/core/utils/generate-random-users.util';
|
||||
import { USER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-users.util';
|
||||
import {
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
SEED_YCOMBINATOR_WORKSPACE_ID,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
import { generateRandomUsers } from 'src/engine/workspace-manager/dev-seeder/core/utils/generate-random-users.util';
|
||||
import { USER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-users.util';
|
||||
|
||||
const tableName = 'userWorkspace';
|
||||
|
||||
@@ -28,11 +28,17 @@ const {
|
||||
|
||||
export const RANDOM_USER_WORKSPACE_IDS = randomUserWorkspaceIds;
|
||||
|
||||
export const seedUserWorkspaces = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
) => {
|
||||
type SeedUserWorkspacesArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const seedUserWorkspaces = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: SeedUserWorkspacesArgs) => {
|
||||
let userWorkspaces: Pick<
|
||||
UserWorkspaceEntity,
|
||||
'id' | 'userId' | 'workspaceId'
|
||||
@@ -89,7 +95,7 @@ export const seedUserWorkspaces = async (
|
||||
},
|
||||
];
|
||||
}
|
||||
await dataSource
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${tableName}`, ['id', 'userId', 'workspaceId'])
|
||||
@@ -98,12 +104,18 @@ export const seedUserWorkspaces = async (
|
||||
.execute();
|
||||
};
|
||||
|
||||
export const deleteUserWorkspaces = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
) => {
|
||||
await dataSource
|
||||
type DeleteUserWorkspacesArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const deleteUserWorkspaces = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: DeleteUserWorkspacesArgs) => {
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(`${schemaName}.${tableName}`)
|
||||
|
||||
+8
-3
@@ -1,4 +1,4 @@
|
||||
import { type DataSource } from 'typeorm';
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
import { generateRandomUsers } from './generate-random-users.util';
|
||||
|
||||
@@ -15,7 +15,12 @@ const { users: randomUsers, userIds: randomUserIds } = generateRandomUsers();
|
||||
|
||||
export const RANDOM_USER_IDS = randomUserIds;
|
||||
|
||||
export const seedUsers = async (dataSource: DataSource, schemaName: string) => {
|
||||
type SeedUsersArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
};
|
||||
|
||||
export const seedUsers = async ({ queryRunner, schemaName }: SeedUsersArgs) => {
|
||||
const originalUsers = [
|
||||
{
|
||||
id: USER_DATA_SEED_IDS.TIM,
|
||||
@@ -65,7 +70,7 @@ export const seedUsers = async (dataSource: DataSource, schemaName: string) => {
|
||||
|
||||
const allUsers = [...originalUsers, ...randomUsers];
|
||||
|
||||
await dataSource
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${tableName}`, [
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
import {
|
||||
type CreateWorkspaceInput,
|
||||
WORKSPACE_FIELDS_TO_SEED,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
|
||||
const tableName = 'workspace';
|
||||
|
||||
export type SeedWorkspaceArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
createWorkspaceInput: CreateWorkspaceInput;
|
||||
};
|
||||
|
||||
export const createWorkspace = async ({
|
||||
schemaName,
|
||||
queryRunner,
|
||||
createWorkspaceInput,
|
||||
}: SeedWorkspaceArgs) => {
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${tableName}`, WORKSPACE_FIELDS_TO_SEED)
|
||||
.orIgnore()
|
||||
.values(createWorkspaceInput)
|
||||
.execute();
|
||||
};
|
||||
|
||||
type DeleteWorkspacesArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const deleteWorkspaces = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: DeleteWorkspacesArgs) => {
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(`${schemaName}.${tableName}`)
|
||||
.where(`${tableName}."id" = :id`, { id: workspaceId })
|
||||
.execute();
|
||||
};
|
||||
-87
@@ -1,87 +0,0 @@
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { type DataSource } from 'typeorm';
|
||||
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { extractVersionMajorMinorPatch } from 'src/utils/version/extract-version-major-minor-patch';
|
||||
|
||||
const tableName = 'workspace';
|
||||
|
||||
export const SEED_APPLE_WORKSPACE_ID = '20202020-1c25-4d02-bf25-6aeccf7ea419';
|
||||
export const SEED_YCOMBINATOR_WORKSPACE_ID =
|
||||
'3b8e6458-5fc1-4e63-8563-008ccddaa6db';
|
||||
|
||||
export type SeedWorkspaceArgs = {
|
||||
dataSource: DataSource;
|
||||
schemaName: string;
|
||||
workspaceId: string;
|
||||
appVersion: string | undefined;
|
||||
};
|
||||
|
||||
const workspaceSeederFields = [
|
||||
'id',
|
||||
'displayName',
|
||||
'subdomain',
|
||||
'inviteHash',
|
||||
'logo',
|
||||
'activationStatus',
|
||||
'version',
|
||||
'isTwoFactorAuthenticationEnforced',
|
||||
] as const satisfies (keyof WorkspaceEntity)[];
|
||||
|
||||
type WorkspaceSeederFields = Pick<
|
||||
WorkspaceEntity,
|
||||
(typeof workspaceSeederFields)[number]
|
||||
>;
|
||||
|
||||
export const seedWorkspaces = async ({
|
||||
schemaName,
|
||||
dataSource,
|
||||
workspaceId,
|
||||
appVersion,
|
||||
}: SeedWorkspaceArgs) => {
|
||||
const version = extractVersionMajorMinorPatch(appVersion);
|
||||
|
||||
const workspaces: Record<string, WorkspaceSeederFields> = {
|
||||
[SEED_APPLE_WORKSPACE_ID]: {
|
||||
id: SEED_APPLE_WORKSPACE_ID,
|
||||
displayName: 'Apple',
|
||||
subdomain: 'apple',
|
||||
inviteHash: 'apple.dev-invite-hash',
|
||||
logo: 'https://twentyhq.github.io/placeholder-images/workspaces/apple-logo.png',
|
||||
activationStatus: WorkspaceActivationStatus.PENDING_CREATION, // will be set to active after default role creation
|
||||
version: version,
|
||||
isTwoFactorAuthenticationEnforced: false,
|
||||
},
|
||||
[SEED_YCOMBINATOR_WORKSPACE_ID]: {
|
||||
id: SEED_YCOMBINATOR_WORKSPACE_ID,
|
||||
displayName: 'YCombinator',
|
||||
subdomain: 'yc',
|
||||
inviteHash: 'yc.dev-invite-hash',
|
||||
logo: 'https://twentyhq.github.io/placeholder-images/workspaces/ycombinator-logo.png',
|
||||
activationStatus: WorkspaceActivationStatus.PENDING_CREATION, // will be set to active after default role creation
|
||||
version: version,
|
||||
isTwoFactorAuthenticationEnforced: false,
|
||||
},
|
||||
};
|
||||
|
||||
await dataSource
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${tableName}`, workspaceSeederFields)
|
||||
.orIgnore()
|
||||
.values(workspaces[workspaceId])
|
||||
.execute();
|
||||
};
|
||||
|
||||
export const deleteWorkspaces = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
) => {
|
||||
await dataSource
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(`${schemaName}.${tableName}`)
|
||||
.where(`${tableName}."id" = :id`, { id: workspaceId })
|
||||
.execute();
|
||||
};
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
import { generateRandomUsers } from 'src/engine/workspace-manager/dev-seeder/core/utils/generate-random-users.util';
|
||||
import { USER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-users.util';
|
||||
import {
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
SEED_YCOMBINATOR_WORKSPACE_ID,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
import { generateRandomUsers } from 'src/engine/workspace-manager/dev-seeder/core/utils/generate-random-users.util';
|
||||
import { USER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-users.util';
|
||||
|
||||
type WorkspaceMemberDataSeed = {
|
||||
id: string;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@@ -36,6 +37,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
|
||||
RoleModule,
|
||||
UserRoleModule,
|
||||
ApiKeyModule,
|
||||
ApplicationModule,
|
||||
FeatureFlagModule,
|
||||
FileStorageModule,
|
||||
WorkspaceSyncMetadataModule,
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/works
|
||||
import {
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
SEED_YCOMBINATOR_WORKSPACE_ID,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
import { COMPANY_CUSTOM_FIELD_SEEDS } from 'src/engine/workspace-manager/dev-seeder/metadata/custom-fields/constants/company-custom-field-seeds.constant';
|
||||
import { PERSON_CUSTOM_FIELD_SEEDS } from 'src/engine/workspace-manager/dev-seeder/metadata/custom-fields/constants/person-custom-field-seeds.constant';
|
||||
import { PET_CUSTOM_FIELD_SEEDS } from 'src/engine/workspace-manager/dev-seeder/metadata/custom-fields/constants/pet-custom-field-seeds.constant';
|
||||
|
||||
+19
-1
@@ -1,8 +1,10 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
@@ -11,6 +13,7 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadat
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { SeededWorkspacesIds } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
import { DevSeederPermissionsService } from 'src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service';
|
||||
import { seedCoreSchema } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-core-schema.util';
|
||||
import { seedPageLayoutTabs } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layout-tabs.util';
|
||||
@@ -18,6 +21,7 @@ import { seedPageLayoutWidgets } from 'src/engine/workspace-manager/dev-seeder/c
|
||||
import { seedPageLayouts } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layouts.util';
|
||||
import { DevSeederDataService } from 'src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service';
|
||||
import { DevSeederMetadataService } from 'src/engine/workspace-manager/dev-seeder/metadata/services/dev-seeder-metadata.service';
|
||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/twenty-standard-applications';
|
||||
import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service';
|
||||
|
||||
@Injectable()
|
||||
@@ -33,17 +37,19 @@ export class DevSeederService {
|
||||
private readonly devSeederPermissionsService: DevSeederPermissionsService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly devSeederDataService: DevSeederDataService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
) {}
|
||||
|
||||
public async seedDev(workspaceId: string): Promise<void> {
|
||||
public async seedDev(workspaceId: SeededWorkspacesIds): Promise<void> {
|
||||
const isBillingEnabled = this.twentyConfigService.get('IS_BILLING_ENABLED');
|
||||
const appVersion = this.twentyConfigService.get('APP_VERSION');
|
||||
|
||||
await seedCoreSchema({
|
||||
dataSource: this.coreDataSource,
|
||||
workspaceId,
|
||||
applicationService: this.applicationService,
|
||||
seedBilling: isBillingEnabled,
|
||||
appVersion,
|
||||
});
|
||||
@@ -62,6 +68,18 @@ export class DevSeederService {
|
||||
const featureFlags =
|
||||
await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId);
|
||||
|
||||
const twentyStandardApplication =
|
||||
await this.applicationService.findByUniversalIdentifier({
|
||||
workspaceId,
|
||||
universalIdentifier: TWENTY_STANDARD_APPLICATION.universalIdentifier,
|
||||
});
|
||||
|
||||
if (!isDefined(twentyStandardApplication)) {
|
||||
throw new Error(
|
||||
'Seeder failed to find twenty standard application, should never occur',
|
||||
);
|
||||
}
|
||||
|
||||
await this.workspaceSyncMetadataService.synchronize({
|
||||
workspaceId: workspaceId,
|
||||
dataSourceId: dataSourceMetadata.id,
|
||||
|
||||
@@ -19,6 +19,7 @@ import { DevSeederModule } from 'src/engine/workspace-manager/dev-seeder/dev-see
|
||||
import { WorkspaceHealthModule } from 'src/engine/workspace-manager/workspace-health/workspace-health.module';
|
||||
import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module';
|
||||
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
|
||||
import { WorkspaceManagerService } from './workspace-manager.service';
|
||||
|
||||
@@ -38,6 +39,7 @@ import { WorkspaceManagerService } from './workspace-manager.service';
|
||||
TypeOrmModule.forFeature([UserWorkspaceEntity, WorkspaceEntity]),
|
||||
RoleModule,
|
||||
UserRoleModule,
|
||||
ApplicationModule,
|
||||
TypeOrmModule.forFeature([
|
||||
FieldMetadataEntity,
|
||||
RoleTargetsEntity,
|
||||
|
||||
@@ -3,10 +3,10 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||
import { type DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
@@ -45,17 +45,18 @@ export class WorkspaceManagerService {
|
||||
private readonly roleRepository: Repository<RoleEntity>,
|
||||
@InjectRepository(RoleTargetsEntity)
|
||||
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
|
||||
private readonly agentService: AgentService,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly applicationService: ApplicationService,
|
||||
) {}
|
||||
|
||||
public async init({
|
||||
workspaceId,
|
||||
workspace,
|
||||
userId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
workspace: WorkspaceEntity;
|
||||
userId: string;
|
||||
}): Promise<void> {
|
||||
const workspaceId = workspace.id;
|
||||
const schemaCreationStart = performance.now();
|
||||
const schemaName =
|
||||
await this.workspaceDataSourceService.createWorkspaceDBSchema(
|
||||
@@ -78,6 +79,11 @@ export class WorkspaceManagerService {
|
||||
const featureFlags =
|
||||
await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId);
|
||||
|
||||
await this.applicationService.createTwentyStandardApplication({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
// TODO later replace by twenty-standard installation aka workspaceMigration run
|
||||
await this.workspaceSyncMetadataService.synchronize({
|
||||
workspaceId,
|
||||
dataSourceId: dataSourceMetadata.id,
|
||||
|
||||
+2
@@ -9,6 +9,7 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works
|
||||
import { WorkspaceHealthModule } from 'src/engine/workspace-manager/workspace-health/workspace-health.module';
|
||||
import { SyncWorkspaceLoggerModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/services/sync-workspace-logger.module';
|
||||
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
|
||||
import { SyncWorkspaceMetadataCommand } from './sync-workspace-metadata.command';
|
||||
|
||||
@@ -22,6 +23,7 @@ import { SyncWorkspaceMetadataCommand } from './sync-workspace-metadata.command'
|
||||
FeatureFlagModule,
|
||||
TypeOrmModule.forFeature([WorkspaceEntity]),
|
||||
SyncWorkspaceLoggerModule,
|
||||
ApplicationModule,
|
||||
],
|
||||
providers: [SyncWorkspaceMetadataCommand],
|
||||
exports: [SyncWorkspaceMetadataCommand],
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { type ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
|
||||
export const TWENTY_STANDARD_APPLICATION = {
|
||||
universalIdentifier: '20202020-64aa-4b6f-b003-9c74b97cee20',
|
||||
name: 'Twenty Standard',
|
||||
description:
|
||||
'Twenty is an open-source CRM that allows you to manage your sales and customer relationships',
|
||||
version: '1.0.0',
|
||||
sourcePath: 'cli-sync',
|
||||
sourceType: 'local',
|
||||
} as const satisfies CreateApplicationInput;
|
||||
|
||||
export type CreateApplicationInput = Omit<
|
||||
ApplicationEntity,
|
||||
| 'workspaceId'
|
||||
| 'serverlessFunctionLayerId'
|
||||
| 'id'
|
||||
| 'createdAt'
|
||||
| 'updatedAt'
|
||||
| 'deletedAt'
|
||||
| 'workspace'
|
||||
| 'agents'
|
||||
| 'applicationVariables'
|
||||
| 'objects'
|
||||
| 'serverlessFunctions'
|
||||
>;
|
||||
export type TwentyStandardApplicationUniversalIdentifiers =
|
||||
(typeof TWENTY_STANDARD_APPLICATION)['universalIdentifier'];
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { type CreateApplicationInput } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/twenty-standard-applications';
|
||||
|
||||
export const computeWorkspaceCustomCreateApplicationInput = ({
|
||||
workspace,
|
||||
applicationId = v4(),
|
||||
}: {
|
||||
applicationId?: string;
|
||||
workspace: Pick<WorkspaceEntity, 'id' | 'displayName'>;
|
||||
}) =>
|
||||
({
|
||||
description: 'Workspace custom application',
|
||||
name: `${isDefined(workspace.displayName) ? `${workspace.displayName}'s ` : ''}custom application`,
|
||||
sourcePath: 'workspace-custom',
|
||||
sourceType: 'local',
|
||||
version: '1.0.0',
|
||||
universalIdentifier: applicationId,
|
||||
workspaceId: workspace.id,
|
||||
id: applicationId,
|
||||
}) as const satisfies CreateApplicationInput & {
|
||||
workspaceId: string;
|
||||
id: string;
|
||||
};
|
||||
+2
@@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@@ -29,6 +30,7 @@ import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/works
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ApplicationModule,
|
||||
FeatureFlagModule,
|
||||
WorkspaceMigrationBuilderModule,
|
||||
WorkspaceMigrationRunnerModule,
|
||||
|
||||
+1
@@ -29,6 +29,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.attachment,
|
||||
|
||||
namePlural: 'attachments',
|
||||
labelSingular: msg`Attachment`,
|
||||
labelPlural: msg`Attachments`,
|
||||
|
||||
+1
@@ -17,6 +17,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.blocklist,
|
||||
|
||||
namePlural: 'blocklists',
|
||||
labelSingular: msg`Blocklist`,
|
||||
labelPlural: msg`Blocklists`,
|
||||
|
||||
+1
@@ -19,6 +19,7 @@ import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standa
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.calendarChannelEventAssociation,
|
||||
|
||||
namePlural: 'calendarChannelEventAssociations',
|
||||
labelSingular: msg`Calendar Channel Event Association`,
|
||||
labelPlural: msg`Calendar Channel Event Associations`,
|
||||
|
||||
+1
@@ -69,6 +69,7 @@ registerEnumType(CalendarChannelContactAutoCreationPolicy, {
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.calendarChannel,
|
||||
|
||||
namePlural: 'calendarChannels',
|
||||
labelSingular: msg`Calendar Channel`,
|
||||
labelPlural: msg`Calendar Channels`,
|
||||
|
||||
+1
@@ -28,6 +28,7 @@ export enum CalendarEventParticipantResponseStatus {
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.calendarEventParticipant,
|
||||
|
||||
namePlural: 'calendarEventParticipants',
|
||||
labelSingular: msg`Calendar event participant`,
|
||||
labelPlural: msg`Calendar event participants`,
|
||||
|
||||
+1
@@ -21,6 +21,7 @@ import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/co
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.calendarEvent,
|
||||
|
||||
namePlural: 'calendarEvents',
|
||||
labelSingular: msg`Calendar event`,
|
||||
labelPlural: msg`Calendar events`,
|
||||
|
||||
+1
@@ -49,6 +49,7 @@ export const SEARCH_FIELDS_FOR_COMPANY: FieldTypeAndNameMetadata[] = [
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.company,
|
||||
|
||||
namePlural: 'companies',
|
||||
labelSingular: msg`Company`,
|
||||
labelPlural: msg`Companies`,
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.connectedAccount,
|
||||
|
||||
namePlural: 'connectedAccounts',
|
||||
labelSingular: msg`Connected Account`,
|
||||
labelPlural: msg`Connected Accounts`,
|
||||
|
||||
+1
@@ -35,6 +35,7 @@ export const SEARCH_FIELDS_FOR_DASHBOARD: FieldTypeAndNameMetadata[] = [
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.dashboard,
|
||||
|
||||
namePlural: 'dashboards',
|
||||
labelSingular: msg`Dashboard`,
|
||||
labelPlural: msg`Dashboards`,
|
||||
|
||||
+1
@@ -15,6 +15,7 @@ import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/f
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.favoriteFolder,
|
||||
|
||||
namePlural: 'favoriteFolders',
|
||||
labelSingular: msg`Favorite Folder`,
|
||||
labelPlural: msg`Favorite Folders`,
|
||||
|
||||
+1
@@ -30,6 +30,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.favorite,
|
||||
|
||||
namePlural: 'favorites',
|
||||
labelSingular: msg`Favorite`,
|
||||
labelPlural: msg`Favorites`,
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-ob
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.messageChannelMessageAssociation,
|
||||
|
||||
namePlural: 'messageChannelMessageAssociations',
|
||||
labelSingular: msg`Message Channel Message Association`,
|
||||
labelPlural: msg`Message Channel Message Associations`,
|
||||
|
||||
+1
@@ -98,6 +98,7 @@ registerEnumType(MessageChannelPendingGroupEmailsAction, {
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.messageChannel,
|
||||
|
||||
namePlural: 'messageChannels',
|
||||
labelSingular: msg`Message Channel`,
|
||||
labelPlural: msg`Message Channels`,
|
||||
|
||||
+1
@@ -31,6 +31,7 @@ registerEnumType(MessageFolderPendingSyncAction, {
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.messageFolder,
|
||||
|
||||
namePlural: 'messageFolders',
|
||||
labelSingular: msg`Message Folder`,
|
||||
labelPlural: msg`Message Folders`,
|
||||
|
||||
+1
@@ -21,6 +21,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.messageParticipant,
|
||||
|
||||
namePlural: 'messageParticipants',
|
||||
labelSingular: msg`Message Participant`,
|
||||
labelPlural: msg`Message Participants`,
|
||||
|
||||
+1
@@ -17,6 +17,7 @@ import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-ob
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.messageThread,
|
||||
|
||||
namePlural: 'messageThreads',
|
||||
labelSingular: msg`Message Thread`,
|
||||
labelPlural: msg`Message Threads`,
|
||||
|
||||
+1
@@ -21,6 +21,7 @@ import { MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/stand
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.message,
|
||||
|
||||
namePlural: 'messages',
|
||||
labelSingular: msg`Message`,
|
||||
labelPlural: msg`Messages`,
|
||||
|
||||
+1
@@ -22,6 +22,7 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.noteTarget,
|
||||
|
||||
namePlural: 'noteTargets',
|
||||
labelSingular: msg`Note Target`,
|
||||
labelPlural: msg`Note Targets`,
|
||||
|
||||
@@ -39,6 +39,7 @@ export const SEARCH_FIELDS_FOR_NOTES: FieldTypeAndNameMetadata[] = [
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.note,
|
||||
|
||||
namePlural: 'notes',
|
||||
labelSingular: msg`Note`,
|
||||
labelPlural: msg`Notes`,
|
||||
|
||||
+1
@@ -42,6 +42,7 @@ export const SEARCH_FIELDS_FOR_OPPORTUNITY: FieldTypeAndNameMetadata[] = [
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.opportunity,
|
||||
|
||||
namePlural: 'opportunities',
|
||||
labelSingular: msg`Opportunity`,
|
||||
labelPlural: msg`Opportunities`,
|
||||
|
||||
@@ -55,6 +55,7 @@ export const SEARCH_FIELDS_FOR_PERSON: FieldTypeAndNameMetadata[] = [
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.person,
|
||||
|
||||
namePlural: 'people',
|
||||
labelSingular: msg`Person`,
|
||||
labelPlural: msg`People`,
|
||||
|
||||
+1
@@ -22,6 +22,7 @@ import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.work
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.taskTarget,
|
||||
|
||||
namePlural: 'taskTargets',
|
||||
labelSingular: msg`Task Target`,
|
||||
labelPlural: msg`Task Targets`,
|
||||
|
||||
@@ -42,6 +42,7 @@ export const SEARCH_FIELDS_FOR_TASKS: FieldTypeAndNameMetadata[] = [
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.task,
|
||||
|
||||
namePlural: 'tasks',
|
||||
labelSingular: msg`Task`,
|
||||
labelPlural: msg`Tasks`,
|
||||
|
||||
+1
@@ -30,6 +30,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.timelineActivity,
|
||||
|
||||
namePlural: 'timelineActivities',
|
||||
labelSingular: msg`Timeline Activity`,
|
||||
labelPlural: msg`Timeline Activities`,
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ export enum AutomatedTriggerType {
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.workflowAutomatedTrigger,
|
||||
|
||||
namePlural: 'workflowAutomatedTriggers',
|
||||
labelSingular: msg`WorkflowAutomatedTrigger`,
|
||||
labelPlural: msg`WorkflowAutomatedTriggers`,
|
||||
|
||||
+1
@@ -81,6 +81,7 @@ export const SEARCH_FIELDS_FOR_WORKFLOW_RUNS: FieldTypeAndNameMetadata[] = [
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.workflowRun,
|
||||
|
||||
namePlural: 'workflowRuns',
|
||||
labelSingular: msg`Workflow Run`,
|
||||
labelPlural: msg`Workflow Runs`,
|
||||
|
||||
+1
@@ -72,6 +72,7 @@ export const SEARCH_FIELDS_FOR_WORKFLOW_VERSIONS: FieldTypeAndNameMetadata[] = [
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.workflowVersion,
|
||||
|
||||
namePlural: 'workflowVersions',
|
||||
labelSingular: msg`Workflow Version`,
|
||||
labelPlural: msg`Workflow Versions`,
|
||||
|
||||
+1
@@ -65,6 +65,7 @@ export const SEARCH_FIELDS_FOR_WORKFLOWS: FieldTypeAndNameMetadata[] = [
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.workflow,
|
||||
|
||||
namePlural: 'workflows',
|
||||
labelSingular: msg`Workflow`,
|
||||
labelPlural: msg`Workflows`,
|
||||
|
||||
+1
@@ -87,6 +87,7 @@ export const SEARCH_FIELDS_FOR_WORKSPACE_MEMBER: FieldTypeAndNameMetadata[] = [
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.workspaceMember,
|
||||
|
||||
namePlural: 'workspaceMembers',
|
||||
labelSingular: msg`Workspace Member`,
|
||||
labelPlural: msg`Workspace Members`,
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Successful User Sign Up (integration) should fail to sign up with duplicate email 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "FORBIDDEN",
|
||||
"subCode": "USER_ALREADY_EXISTS",
|
||||
"userFriendlyMessage": "User already exists",
|
||||
},
|
||||
"message": "User already exists",
|
||||
"name": "ForbiddenError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Successful User Sign Up (integration) should fail to sign up with invalid email 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "email must be an email",
|
||||
"name": "UserInputError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Successful User Sign Up (integration) should fail to sign up with weak password 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_INPUT",
|
||||
"userFriendlyMessage": "Password too weak",
|
||||
},
|
||||
"message": "Password too weak",
|
||||
"name": "UserInputError",
|
||||
}
|
||||
`;
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Successful User Sign Up (integration) should sign up, delete and signup same new user successfully 1`] = `
|
||||
{
|
||||
"canAccessFullAdminPanel": false,
|
||||
"canImpersonate": false,
|
||||
"createdAt": Any<String>,
|
||||
"currentUserWorkspace": null,
|
||||
"currentWorkspace": null,
|
||||
"defaultAvatarUrl": null,
|
||||
"deletedAt": null,
|
||||
"disabled": false,
|
||||
"email": "test-123@example.com",
|
||||
"firstName": "",
|
||||
"id": Any<String>,
|
||||
"isEmailVerified": false,
|
||||
"lastName": "",
|
||||
"locale": "en",
|
||||
"updatedAt": Any<String>,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Successful User Sign Up (integration) should sign up, delete and signup same new user successfully 2`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "UNAUTHENTICATED",
|
||||
"subCode": "USER_NOT_FOUND",
|
||||
},
|
||||
"message": "User not found",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Successful User Sign Up (integration) should sign up, delete and signup same new user successfully 3`] = `
|
||||
{
|
||||
"canAccessFullAdminPanel": false,
|
||||
"canImpersonate": false,
|
||||
"createdAt": Any<String>,
|
||||
"currentUserWorkspace": null,
|
||||
"currentWorkspace": null,
|
||||
"defaultAvatarUrl": null,
|
||||
"deletedAt": null,
|
||||
"disabled": false,
|
||||
"email": "test-123@example.com",
|
||||
"firstName": "",
|
||||
"id": Any<String>,
|
||||
"isEmailVerified": false,
|
||||
"lastName": "",
|
||||
"locale": "en",
|
||||
"updatedAt": Any<String>,
|
||||
}
|
||||
`;
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
|
||||
import { signUp } from 'test/integration/graphql/utils/sign-up.util';
|
||||
|
||||
describe('Successful User Sign Up (integration)', () => {
|
||||
it('should fail to sign up with invalid email', async () => {
|
||||
const { errors } = await signUp({
|
||||
input: {
|
||||
email: 'invalid-email',
|
||||
password: 'Test123!@#',
|
||||
},
|
||||
expectToFail: true,
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({
|
||||
errors,
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to sign up with duplicate email', async () => {
|
||||
const testEmail = `test-duplicate@example.com`;
|
||||
|
||||
const { data: firstSignUp } = await signUp({
|
||||
input: {
|
||||
email: testEmail,
|
||||
password: 'Test123!@#',
|
||||
},
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
expect(
|
||||
firstSignUp.signUp.tokens.accessOrWorkspaceAgnosticToken.token,
|
||||
).toBeDefined();
|
||||
|
||||
const { errors } = await signUp({
|
||||
input: {
|
||||
email: testEmail,
|
||||
password: 'AnotherPassword123!',
|
||||
},
|
||||
expectToFail: true,
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({
|
||||
errors,
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to sign up with weak password', async () => {
|
||||
const { errors } = await signUp({
|
||||
input: {
|
||||
email: `test-123@example.com`,
|
||||
password: '123',
|
||||
},
|
||||
expectToFail: true,
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({
|
||||
errors,
|
||||
});
|
||||
});
|
||||
});
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
import { deleteUser } from 'test/integration/graphql/utils/delete-user.util';
|
||||
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
|
||||
import { getCurrentUser } from 'test/integration/graphql/utils/get-current-user.util';
|
||||
import { signUp } from 'test/integration/graphql/utils/sign-up.util';
|
||||
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type SignUpInput } from 'src/engine/core-modules/auth/dto/sign-up.input';
|
||||
|
||||
describe('Successful User Sign Up (integration)', () => {
|
||||
let createdUserAccessToken: string | undefined;
|
||||
|
||||
afterEach(async () => {
|
||||
if (isDefined(createdUserAccessToken)) {
|
||||
await deleteUser({
|
||||
accessToken: createdUserAccessToken,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
createdUserAccessToken = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
it('should sign up, delete and signup same new user successfully', async () => {
|
||||
const input: SignUpInput = {
|
||||
email: `test-123@example.com`,
|
||||
password: 'Test123!@#',
|
||||
};
|
||||
|
||||
const { data: firstSignUpData } = await signUp({
|
||||
input,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
createdUserAccessToken =
|
||||
firstSignUpData.signUp.tokens.accessOrWorkspaceAgnosticToken.token;
|
||||
|
||||
expect(
|
||||
firstSignUpData.signUp.tokens.accessOrWorkspaceAgnosticToken.token,
|
||||
).toBeDefined();
|
||||
expect(firstSignUpData.signUp.tokens.refreshToken.token).toBeDefined();
|
||||
expect(
|
||||
firstSignUpData.signUp.availableWorkspaces.availableWorkspacesForSignIn,
|
||||
).toEqual([]);
|
||||
expect(
|
||||
firstSignUpData.signUp.availableWorkspaces.availableWorkspacesForSignUp,
|
||||
).toEqual([]);
|
||||
|
||||
const {
|
||||
data: { currentUser: currentUserAfterSignUp },
|
||||
} = await getCurrentUser({
|
||||
accessToken: createdUserAccessToken,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
expect(currentUserAfterSignUp.deletedAt).toBeNull();
|
||||
expect(currentUserAfterSignUp).toMatchSnapshot(
|
||||
extractRecordIdsAndDatesAsExpectAny({ ...currentUserAfterSignUp }),
|
||||
);
|
||||
|
||||
await deleteUser({
|
||||
accessToken: createdUserAccessToken,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const { errors: getCurrentUserAfterDeleteErrors } = await getCurrentUser({
|
||||
accessToken: createdUserAccessToken,
|
||||
expectToFail: true,
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({
|
||||
errors: getCurrentUserAfterDeleteErrors,
|
||||
});
|
||||
|
||||
const { data: secondSignUpData } = await signUp({
|
||||
input,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
createdUserAccessToken =
|
||||
secondSignUpData.signUp.tokens.accessOrWorkspaceAgnosticToken.token;
|
||||
|
||||
const {
|
||||
data: { currentUser: currentUserAfterSecondSignUp },
|
||||
} = await getCurrentUser({
|
||||
accessToken: createdUserAccessToken,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
expect(currentUserAfterSecondSignUp.deletedAt).toBeNull();
|
||||
expect(currentUserAfterSecondSignUp).toMatchSnapshot(
|
||||
extractRecordIdsAndDatesAsExpectAny({ ...currentUserAfterSecondSignUp }),
|
||||
);
|
||||
});
|
||||
});
|
||||
+46
-45
@@ -20,11 +20,13 @@ import {
|
||||
TEST_PET_ID_3,
|
||||
TEST_PET_ID_4,
|
||||
} from 'test/integration/constants/test-pet-ids.constants';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { performCreateManyOperation } from 'test/integration/graphql/utils/perform-create-many-operation.utils';
|
||||
import { searchFactory } from 'test/integration/graphql/utils/search-factory.util';
|
||||
import { search } from 'test/integration/graphql/utils/search.util';
|
||||
import { deleteAllRecords } from 'test/integration/utils/delete-all-records';
|
||||
import { type EachTestingContext } from 'twenty-shared/testing';
|
||||
import {
|
||||
eachTestingContextFilter,
|
||||
type EachTestingContext,
|
||||
} from 'twenty-shared/testing';
|
||||
|
||||
import {
|
||||
decodeCursor,
|
||||
@@ -909,45 +911,49 @@ describe('SearchResolver', () => {
|
||||
},
|
||||
];
|
||||
|
||||
it.each(testsUseCases)('$title', async ({ context }) => {
|
||||
const graphqlOperation = searchFactory(context.input);
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
it.each(eachTestingContextFilter(testsUseCases))(
|
||||
'$title',
|
||||
async ({ context }) => {
|
||||
const response = await search({
|
||||
...context.input,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
expect(response.body.data).toBeDefined();
|
||||
expect(response.body.data.search).toBeDefined();
|
||||
expect(response.data).toBeDefined();
|
||||
expect(response.data.search).toBeDefined();
|
||||
|
||||
const search = response.body.data.search;
|
||||
const edges = search.edges;
|
||||
const pageInfo = search.pageInfo;
|
||||
const searchResult = response.data.search;
|
||||
const edges = searchResult.edges;
|
||||
const pageInfo = searchResult.pageInfo;
|
||||
|
||||
if (context.eval.orderedRecordIds.length > 0) {
|
||||
expect(edges).not.toHaveLength(0);
|
||||
} else {
|
||||
expect(edges).toHaveLength(0);
|
||||
}
|
||||
if (context.eval.orderedRecordIds.length > 0) {
|
||||
expect(edges).not.toHaveLength(0);
|
||||
} else {
|
||||
expect(edges).toHaveLength(0);
|
||||
}
|
||||
|
||||
expect(
|
||||
edges.map((edge: SearchResultEdgeDTO) => edge.node.recordId),
|
||||
).toEqual(context.eval.orderedRecordIds);
|
||||
expect(
|
||||
edges.map((edge: SearchResultEdgeDTO) => edge.node.recordId),
|
||||
).toEqual(context.eval.orderedRecordIds);
|
||||
|
||||
expect(pageInfo).toBeDefined();
|
||||
expect(context.eval.pageInfo.hasNextPage).toEqual(pageInfo.hasNextPage);
|
||||
expect(context.eval.pageInfo.decodedEndCursor).toEqual(
|
||||
pageInfo.endCursor
|
||||
? decodeCursor(pageInfo.endCursor)
|
||||
: pageInfo.endCursor,
|
||||
);
|
||||
});
|
||||
expect(pageInfo).toBeDefined();
|
||||
expect(context.eval.pageInfo.hasNextPage).toEqual(pageInfo.hasNextPage);
|
||||
expect(context.eval.pageInfo.decodedEndCursor).toEqual(
|
||||
pageInfo.endCursor
|
||||
? decodeCursor(pageInfo.endCursor)
|
||||
: pageInfo.endCursor,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it('should return cursor for each search edge', async () => {
|
||||
const graphqlOperation = searchFactory({
|
||||
const response = await search({
|
||||
searchInput: 'searchInput',
|
||||
excludedObjectNameSingulars: ['workspaceMember'],
|
||||
limit: 2,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
|
||||
const expectedResult = {
|
||||
edges: [
|
||||
{
|
||||
@@ -979,17 +985,15 @@ describe('SearchResolver', () => {
|
||||
};
|
||||
|
||||
expect({
|
||||
...response.body.data.search,
|
||||
edges: response.body.data.search.edges.map(
|
||||
(edge: SearchResultEdgeDTO) => ({
|
||||
cursor: edge.cursor,
|
||||
}),
|
||||
),
|
||||
...response.data.search,
|
||||
edges: response.data.search.edges.map((edge: SearchResultEdgeDTO) => ({
|
||||
cursor: edge.cursor,
|
||||
})),
|
||||
}).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should return cursor for each search edge with after cursor input', async () => {
|
||||
const graphqlOperation = searchFactory({
|
||||
const response = await search({
|
||||
searchInput: 'searchInput',
|
||||
excludedObjectNameSingulars: ['workspaceMember'],
|
||||
limit: 2,
|
||||
@@ -999,10 +1003,9 @@ describe('SearchResolver', () => {
|
||||
person: searchInput2Person.id,
|
||||
},
|
||||
}),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
|
||||
const expectedResult = {
|
||||
edges: [
|
||||
{
|
||||
@@ -1036,12 +1039,10 @@ describe('SearchResolver', () => {
|
||||
};
|
||||
|
||||
expect({
|
||||
...response.body.data.search,
|
||||
edges: response.body.data.search.edges.map(
|
||||
(edge: SearchResultEdgeDTO) => ({
|
||||
cursor: edge.cursor,
|
||||
}),
|
||||
),
|
||||
...response.data.search,
|
||||
edges: response.data.search.edges.map((edge: SearchResultEdgeDTO) => ({
|
||||
cursor: edge.cursor,
|
||||
})),
|
||||
}).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import { activateWorkspace } from 'test/integration/graphql/utils/activate-workspace.util';
|
||||
import { deleteUser } from 'test/integration/graphql/utils/delete-user.util';
|
||||
import { findManyApplications } from 'test/integration/graphql/utils/find-many-applications.util';
|
||||
import { getAuthTokensFromLoginToken } from 'test/integration/graphql/utils/get-auth-tokens-from-login-token.util';
|
||||
import { getCurrentUser } from 'test/integration/graphql/utils/get-current-user.util';
|
||||
import { signUpInNewWorkspace } from 'test/integration/graphql/utils/sign-up-in-new-workspace.util';
|
||||
import { signUp } from 'test/integration/graphql/utils/sign-up.util';
|
||||
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
|
||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/twenty-standard-applications';
|
||||
|
||||
describe('Successful user and workspace creation', () => {
|
||||
let createdUserAccessToken: string | undefined;
|
||||
|
||||
afterEach(async () => {
|
||||
if (!isDefined(createdUserAccessToken)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await deleteUser({
|
||||
accessToken: createdUserAccessToken,
|
||||
expectToFail: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should sign up a new user and create a new workspace successfully', async () => {
|
||||
const { data } = await signUp({
|
||||
input: {
|
||||
email: `test-1234@example.com`,
|
||||
password: 'Test123!@#',
|
||||
},
|
||||
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
createdUserAccessToken =
|
||||
data.signUp.tokens.accessOrWorkspaceAgnosticToken.token;
|
||||
|
||||
const {
|
||||
data: { signUpInNewWorkspace: signUpInNewWorkspaceData },
|
||||
} = await signUpInNewWorkspace({
|
||||
accessToken: createdUserAccessToken,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const {
|
||||
data: { getAuthTokensFromLoginToken: authTokensData },
|
||||
} = await getAuthTokensFromLoginToken({
|
||||
origin: signUpInNewWorkspaceData.workspace.workspaceUrls.subdomainUrl,
|
||||
loginToken: signUpInNewWorkspaceData.loginToken.token,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const newWorkspaceAccessToken =
|
||||
authTokensData.tokens.accessOrWorkspaceAgnosticToken.token;
|
||||
|
||||
const {
|
||||
data: { activateWorkspace: activateWorkspaceData },
|
||||
} = await activateWorkspace({
|
||||
accessToken: newWorkspaceAccessToken,
|
||||
displayName: '42 answer',
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
expect(activateWorkspaceData.activationStatus).toBe(
|
||||
WorkspaceActivationStatus.ACTIVE,
|
||||
);
|
||||
|
||||
const {
|
||||
data: { currentUser },
|
||||
} = await getCurrentUser({
|
||||
accessToken: newWorkspaceAccessToken,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
jestExpectToBeDefined(currentUser.currentWorkspace);
|
||||
const { inviteHash: _, ...expectedCurrentWorkspace } =
|
||||
activateWorkspaceData;
|
||||
|
||||
jestExpectToBeDefined(currentUser.currentWorkspace);
|
||||
expect(currentUser.currentWorkspace).toMatchObject(
|
||||
expectedCurrentWorkspace,
|
||||
);
|
||||
|
||||
const {
|
||||
data: { findManyApplications: findManyApplicationsData },
|
||||
} = await findManyApplications({
|
||||
accessToken: newWorkspaceAccessToken,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
expect(findManyApplicationsData.length).toBe(2);
|
||||
const twentyStandardApp = findManyApplicationsData.find(
|
||||
(application) =>
|
||||
application.universalIdentifier ===
|
||||
TWENTY_STANDARD_APPLICATION.universalIdentifier,
|
||||
);
|
||||
|
||||
jestExpectToBeDefined(twentyStandardApp);
|
||||
const {
|
||||
sourcePath: _sourcePath,
|
||||
sourceType: _sourceType,
|
||||
...expectedStandardTwentyApplication
|
||||
} = TWENTY_STANDARD_APPLICATION;
|
||||
|
||||
expect(twentyStandardApp).toMatchObject(expectedStandardTwentyApplication);
|
||||
|
||||
const workpsaceCustomApplication = findManyApplicationsData.find(
|
||||
(application) =>
|
||||
application.id ===
|
||||
currentUser.currentWorkspace?.workspaceCustomApplicationId,
|
||||
);
|
||||
|
||||
jestExpectToBeDefined(workpsaceCustomApplication);
|
||||
expect(workpsaceCustomApplication.universalIdentifier).toEqual(
|
||||
workpsaceCustomApplication.id,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
type ActivateWorkspaceUtilArgs = {
|
||||
accessToken: string;
|
||||
displayName: string;
|
||||
expectToFail?: boolean;
|
||||
};
|
||||
|
||||
export const activateWorkspace = async ({
|
||||
accessToken,
|
||||
displayName,
|
||||
expectToFail,
|
||||
}: ActivateWorkspaceUtilArgs): CommonResponseBody<{
|
||||
activateWorkspace: WorkspaceEntity;
|
||||
}> => {
|
||||
const mutation = gql`
|
||||
mutation ActivateWorkspace($input: ActivateWorkspaceInput!) {
|
||||
activateWorkspace(data: $input) {
|
||||
id
|
||||
displayName
|
||||
activationStatus
|
||||
subdomain
|
||||
inviteHash
|
||||
logo
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await makeGraphqlAPIRequest(
|
||||
{
|
||||
query: mutation,
|
||||
variables: {
|
||||
input: {
|
||||
displayName,
|
||||
},
|
||||
},
|
||||
},
|
||||
accessToken,
|
||||
);
|
||||
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Activate workspace should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Activate workspace has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
|
||||
import { type UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
|
||||
type DeleteUserUtilArgs = {
|
||||
accessToken: string;
|
||||
expectToFail?: boolean;
|
||||
};
|
||||
|
||||
export const deleteUser = async ({
|
||||
accessToken,
|
||||
expectToFail,
|
||||
}: DeleteUserUtilArgs): CommonResponseBody<{
|
||||
deleteUser: UserEntity;
|
||||
}> => {
|
||||
const mutation = gql`
|
||||
mutation DeleteUser {
|
||||
deleteUser {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
deletedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await makeGraphqlAPIRequest(
|
||||
{
|
||||
query: mutation,
|
||||
variables: {},
|
||||
},
|
||||
accessToken,
|
||||
);
|
||||
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Delete user should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Delete user has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { type ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto';
|
||||
|
||||
export const APPLICATION_GQL_FIELDS = `
|
||||
id
|
||||
name
|
||||
description
|
||||
version
|
||||
universalIdentifier
|
||||
`;
|
||||
|
||||
export const findManyApplications = async ({
|
||||
gqlFields = APPLICATION_GQL_FIELDS,
|
||||
expectToFail,
|
||||
accessToken,
|
||||
}: {
|
||||
gqlFields?: string;
|
||||
expectToFail?: boolean;
|
||||
accessToken?: string;
|
||||
}): CommonResponseBody<{
|
||||
findManyApplications: ApplicationDTO[];
|
||||
}> => {
|
||||
const response = await makeGraphqlAPIRequest(
|
||||
{
|
||||
query: gql`
|
||||
query FindManyApplications {
|
||||
findManyApplications {
|
||||
${gqlFields}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {},
|
||||
},
|
||||
accessToken,
|
||||
);
|
||||
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Application search should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Application search has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
|
||||
import { type AuthTokens } from 'src/engine/core-modules/auth/dto/auth-tokens.dto';
|
||||
|
||||
type GetAuthTokensFromLoginTokenUtilArgs = {
|
||||
loginToken: string;
|
||||
origin?: string;
|
||||
expectToFail?: boolean;
|
||||
};
|
||||
|
||||
export const getAuthTokensFromLoginToken = async ({
|
||||
loginToken,
|
||||
origin = 'http://localhost:3001',
|
||||
expectToFail,
|
||||
}: GetAuthTokensFromLoginTokenUtilArgs): CommonResponseBody<{
|
||||
getAuthTokensFromLoginToken: AuthTokens;
|
||||
}> => {
|
||||
const mutation = gql`
|
||||
mutation GetAuthTokensFromLoginToken(
|
||||
$loginToken: String!
|
||||
$origin: String!
|
||||
) {
|
||||
getAuthTokensFromLoginToken(loginToken: $loginToken, origin: $origin) {
|
||||
tokens {
|
||||
accessOrWorkspaceAgnosticToken {
|
||||
token
|
||||
expiresAt
|
||||
}
|
||||
refreshToken {
|
||||
token
|
||||
expiresAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await makeGraphqlAPIRequest(
|
||||
{
|
||||
query: mutation,
|
||||
variables: {
|
||||
loginToken,
|
||||
origin,
|
||||
},
|
||||
},
|
||||
undefined, // Public endpoint - no authentication required
|
||||
);
|
||||
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage:
|
||||
'Get auth tokens from login token should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage:
|
||||
'Get auth tokens from login token has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
|
||||
import { type UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
|
||||
type CurrentUserUtilArgs = {
|
||||
accessToken: string;
|
||||
expectToFail?: boolean;
|
||||
};
|
||||
|
||||
export const getCurrentUser = async ({
|
||||
accessToken,
|
||||
expectToFail,
|
||||
}: CurrentUserUtilArgs): CommonResponseBody<{
|
||||
currentUser: UserEntity;
|
||||
}> => {
|
||||
const query = gql`
|
||||
query CurrentUser {
|
||||
currentUser {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
defaultAvatarUrl
|
||||
isEmailVerified
|
||||
disabled
|
||||
canImpersonate
|
||||
canAccessFullAdminPanel
|
||||
locale
|
||||
createdAt
|
||||
updatedAt
|
||||
deletedAt
|
||||
currentWorkspace {
|
||||
id
|
||||
displayName
|
||||
subdomain
|
||||
activationStatus
|
||||
logo
|
||||
workspaceCustomApplicationId
|
||||
}
|
||||
currentUserWorkspace {
|
||||
id
|
||||
userId
|
||||
workspaceId
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await makeGraphqlAPIRequest(
|
||||
{
|
||||
query,
|
||||
variables: {},
|
||||
},
|
||||
accessToken,
|
||||
);
|
||||
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Get current user should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Get current user has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
+15
-8
@@ -1,19 +1,26 @@
|
||||
import { type ASTNode, print } from 'graphql';
|
||||
import request from 'supertest';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type GraphqlOperation = {
|
||||
query: ASTNode;
|
||||
variables?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export const makeGraphqlAPIRequest = (graphqlOperation: GraphqlOperation) => {
|
||||
export const makeGraphqlAPIRequest = (
|
||||
graphqlOperation: GraphqlOperation,
|
||||
token: string | undefined = APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
) => {
|
||||
const client = request(`http://localhost:${APP_PORT}`);
|
||||
|
||||
return client
|
||||
.post('/graphql')
|
||||
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
|
||||
.send({
|
||||
query: print(graphqlOperation.query),
|
||||
variables: graphqlOperation.variables || {},
|
||||
});
|
||||
const clientInstance = client.post('/graphql');
|
||||
|
||||
if (isDefined(token)) {
|
||||
clientInstance.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
|
||||
return clientInstance.send({
|
||||
query: print(graphqlOperation.query),
|
||||
variables: graphqlOperation.variables || {},
|
||||
});
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user