Runner v2 on cascade delete_field/object + refactor workspace deletion side effect (#15830)

# Introduction
While removing the v1 https://github.com/twentyhq/twenty/pull/15823 I've
encountered the method `objectMetadataService.deleteObjectsMetadata`
that I didn't wanted to migrate as it is and if it's not challenging its
existence legitimacy.

## Motivations
In a nutshell on a workspace deletion object metadata and field metadata
cascading deletion is correclty handled
But that's not the case for all of a workspaces entities ( roles,
workspaceMigrations ) I suspect that we did not defined the foreignKey
explicitly through `typeorm`

## Battle testing v2
I still decided to give a try to a complex operation in the v2 such as a
workspace all object metadata deletion.
Spoiler it failed due to object being interdependent between them and
not being topologically sorted ( morph relation can introduce circular
dep anw ).
Note: Even after removing the delete field on delete object aggregator
we end up with an equivalent circular dep error which an object and its
field metadata identifier connection.

## Elegant `DEFERRED` and `DEFERRABLE` foreign keys
The most safe, low level solution would be to make all field relations
deferrable and start the runner transaction as deferred. But this
requires a quite invasive migration of existing FK

## On cascade solution
I've opted for the quick fix, on object or field deletion spread
cacasde.
It's pretty safe as the builder priorly validates the deletion and and
its related entities integrity
Only for both field and object deletion action types in v2 runner

## Integration coverage
Added a test that will scan a new workspace database core schema tables
and expect now result after workspace deletion through its only user
deletion
This commit is contained in:
Paul Rastoin
2025-11-16 22:03:50 +01:00
committed by GitHub
parent baa1a1e52f
commit 48031a7ce2
15 changed files with 316 additions and 71 deletions
@@ -9,7 +9,6 @@ import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { Repository } from 'typeorm';
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
import { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/dns-manager.service';
@@ -51,7 +50,6 @@ import { extractVersionMajorMinorPatch } from 'src/utils/version/extract-version
@Injectable()
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
private readonly featureLookUpKey = BillingEntitlementKey.CUSTOM_DOMAIN;
protected readonly logger = new Logger(WorkspaceService.name);
private readonly WORKSPACE_FIELD_PERMISSIONS: Record<
@@ -1,11 +1,12 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { fromArrayToUniqueKeyRecord, isDefined } from 'twenty-shared/utils';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { computeFlatEntityMapsFromTo } from 'src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util';
import { fromDeleteObjectInputToFlatFieldMetadatasToDelete } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-delete-object-input-to-flat-field-metadatas-to-delete.util';
@@ -165,50 +166,127 @@ export class ObjectMetadataServiceV2 {
workspaceId: string;
isSystemBuild?: boolean;
}): Promise<ObjectMetadataDTO> {
const deletedObjectMetadataDtos = await this.deleteManyObjectMetadatas({
deleteObjectInputs: [deleteObjectInput],
workspaceId,
isSystemBuild,
});
if (deletedObjectMetadataDtos.length !== 1) {
throw new ObjectMetadataException(
'Could not retrieve deleted object metadata dto',
ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR,
);
}
const [deletedObjectMetadataDto] = deletedObjectMetadataDtos;
return deletedObjectMetadataDto;
}
private async deleteManyObjectMetadatas({
workspaceId,
deleteObjectInputs,
isSystemBuild = false,
}: {
deleteObjectInputs: DeleteOneObjectInput[];
workspaceId: string;
isSystemBuild?: boolean;
}): Promise<ObjectMetadataDTO[]> {
if (deleteObjectInputs.length === 0) {
return [];
}
const { flatObjectMetadataMaps, flatFieldMetadataMaps, flatIndexMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatObjectMetadataMaps',
'flatIndexMaps',
'flatFieldMetadataMaps',
],
},
);
const initialAccumulator: {
flatFieldMetadatasToDeleteById: Record<string, FlatFieldMetadata>;
flatObjectMetadatasToDeleteById: Record<string, FlatObjectMetadata>;
flatIndexToDeleteById: Record<string, FlatIndexMetadata>;
} = {
flatFieldMetadatasToDeleteById: {},
flatIndexToDeleteById: {},
flatObjectMetadatasToDeleteById: {},
};
const {
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
flatIndexMaps: existingFlatIndexMaps,
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatObjectMetadataMaps',
'flatIndexMaps',
'flatFieldMetadataMaps',
],
},
);
flatFieldMetadatasToDeleteById,
flatIndexToDeleteById,
flatObjectMetadatasToDeleteById,
} = deleteObjectInputs.reduce((accumulator, deleteObjectInput) => {
const {
flatFieldMetadatasToDelete,
flatObjectMetadataToDelete,
flatIndexToDelete,
} = fromDeleteObjectInputToFlatFieldMetadatasToDelete({
flatObjectMetadataMaps,
flatIndexMaps,
flatFieldMetadataMaps,
deleteObjectInput,
});
return {
flatFieldMetadatasToDeleteById: {
...accumulator.flatFieldMetadatasToDeleteById,
...fromArrayToUniqueKeyRecord({
array: flatFieldMetadatasToDelete,
uniqueKey: 'id',
}),
},
flatIndexToDeleteById: {
...accumulator.flatIndexToDeleteById,
...fromArrayToUniqueKeyRecord({
array: flatIndexToDelete,
uniqueKey: 'id',
}),
},
flatObjectMetadatasToDeleteById: {
...accumulator.flatObjectMetadatasToDeleteById,
[flatObjectMetadataToDelete.id]: flatObjectMetadataToDelete,
},
};
}, initialAccumulator);
const {
flatFieldMetadatasToDelete,
flatObjectMetadataToDelete,
flatObjectMetadatasToDelete,
flatIndexToDelete,
} = fromDeleteObjectInputToFlatFieldMetadatasToDelete({
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
flatIndexMaps: existingFlatIndexMaps,
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
deleteObjectInput,
});
} = {
flatFieldMetadatasToDelete: Object.values(flatFieldMetadatasToDeleteById),
flatObjectMetadatasToDelete: Object.values(
flatObjectMetadatasToDeleteById,
),
flatIndexToDelete: Object.values(flatIndexToDeleteById),
};
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromToAllFlatEntityMaps: {
flatObjectMetadataMaps: computeFlatEntityMapsFromTo({
flatEntityMaps: existingFlatObjectMetadataMaps,
flatEntityMaps: flatObjectMetadataMaps,
flatEntityToCreate: [],
flatEntityToDelete: [flatObjectMetadataToDelete],
flatEntityToDelete: flatObjectMetadatasToDelete,
flatEntityToUpdate: [],
}),
flatIndexMaps: computeFlatEntityMapsFromTo({
flatEntityMaps: existingFlatIndexMaps,
flatEntityMaps: flatIndexMaps,
flatEntityToCreate: [],
flatEntityToDelete: flatIndexToDelete,
flatEntityToUpdate: [],
}),
flatFieldMetadataMaps: computeFlatEntityMapsFromTo({
flatEntityMaps: existingFlatFieldMetadataMaps,
flatEntityMaps: flatFieldMetadataMaps,
flatEntityToCreate: [],
flatEntityToDelete: flatFieldMetadatasToDelete,
flatEntityToUpdate: [],
@@ -229,12 +307,12 @@ export class ObjectMetadataServiceV2 {
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while deleting object',
`Multiple validation errors occurred while deleting object${deleteObjectInputs.length > 1 ? 's' : ''}`,
);
}
return fromFlatObjectMetadataToObjectMetadataDto(
flatObjectMetadataToDelete,
return flatObjectMetadatasToDelete.map(
fromFlatObjectMetadataToObjectMetadataDto,
);
}
@@ -445,4 +523,30 @@ export class ObjectMetadataServiceV2 {
position: favoriteCount,
});
}
public async deleteWorkspaceAllObjectMetadata({
workspaceId,
}: {
workspaceId: string;
}) {
const { flatObjectMetadataMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatObjectMetadataMaps'],
},
);
const deleteObjectInputs = Object.values(flatObjectMetadataMaps.byId)
.filter(isDefined)
.map<DeleteOneObjectInput>((flatObjectMetadata) => ({
id: flatObjectMetadata.id,
}));
await this.deleteManyObjectMetadatas({
deleteObjectInputs,
workspaceId,
isSystemBuild: true,
});
}
}
@@ -107,6 +107,7 @@ export class ObjectMetadataEntity
@Column({ default: false })
isLabelSyncedWithName: boolean;
// TODO create a relation to workspace with cascade delete will also create foreignKey
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
@@ -33,20 +33,23 @@ export class WorkspaceSchemaColumnManagerService {
schemaName,
tableName,
columnNames,
cascade = false,
}: {
queryRunner: QueryRunner;
schemaName: string;
tableName: string;
columnNames: string[];
cascade?: boolean;
}): Promise<void> {
if (columnNames.length === 0) return;
const safeSchemaName = removeSqlDDLInjection(schemaName);
const safeTableName = removeSqlDDLInjection(tableName);
const cascadeClause = cascade ? ' CASCADE' : '';
const dropClauses = columnNames.map((name) => {
const safeName = removeSqlDDLInjection(name);
return `DROP COLUMN IF EXISTS "${safeName}"`;
return `DROP COLUMN IF EXISTS "${safeName}"${cascadeClause}`;
});
const sql = `ALTER TABLE "${safeSchemaName}"."${safeTableName}" ${dropClauses.join(', ')}`;
@@ -39,14 +39,17 @@ export class WorkspaceSchemaTableManagerService {
queryRunner,
schemaName,
tableName,
cascade = false,
}: {
queryRunner: QueryRunner;
schemaName: string;
tableName: string;
cascade?: boolean;
}): Promise<void> {
const safeSchemaName = removeSqlDDLInjection(schemaName);
const safeTableName = removeSqlDDLInjection(tableName);
const sql = `DROP TABLE IF EXISTS "${safeSchemaName}"."${safeTableName}"`;
const cascadeClause = cascade ? ' CASCADE' : '';
const sql = `DROP TABLE IF EXISTS "${safeSchemaName}"."${safeTableName}"${cascadeClause}`;
await queryRunner.query(sql);
}
@@ -11,6 +11,7 @@ import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
@@ -27,7 +28,7 @@ import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/works
describe('WorkspaceManagerService', () => {
let service: WorkspaceManagerService;
let objectMetadataService: ObjectMetadataService;
let objectMetadataServiceV2: ObjectMetadataServiceV2;
let workspaceMigrationRepository: Repository<WorkspaceMigrationEntity>;
let dataSourceRepository: Repository<DataSourceEntity>;
let workspaceDataSourceService: WorkspaceDataSourceService;
@@ -101,6 +102,10 @@ describe('WorkspaceManagerService', () => {
provide: FeatureFlagService,
useValue: {},
},
{
provide: ObjectMetadataService,
useValue: {},
},
{
provide: RoleService,
useValue: {},
@@ -123,11 +128,10 @@ describe('WorkspaceManagerService', () => {
provide: WorkspaceSyncMetadataService,
useValue: {},
},
{
provide: ObjectMetadataService,
provide: ObjectMetadataServiceV2,
useValue: {
deleteObjectsMetadata: jest.fn(),
deleteWorkspaceAllObjectMetadata: jest.fn(),
},
},
{
@@ -150,8 +154,8 @@ describe('WorkspaceManagerService', () => {
}).compile();
service = module.get<WorkspaceManagerService>(WorkspaceManagerService);
objectMetadataService = module.get<ObjectMetadataService>(
ObjectMetadataService,
objectMetadataServiceV2 = module.get<ObjectMetadataServiceV2>(
ObjectMetadataServiceV2,
);
workspaceMigrationRepository = module.get<
Repository<WorkspaceMigrationEntity>
@@ -177,7 +181,9 @@ describe('WorkspaceManagerService', () => {
describe('delete', () => {
it('should delete all the workspace metadata tables and workspace schema', async () => {
await service.delete('workspace-id');
expect(objectMetadataService.deleteObjectsMetadata).toHaveBeenCalled();
expect(
objectMetadataServiceV2.deleteWorkspaceAllObjectMetadata,
).toHaveBeenCalled();
expect(workspaceMigrationRepository.delete).toHaveBeenCalledWith({
workspaceId: 'workspace-id',
});
@@ -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 { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -18,7 +19,6 @@ 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';
@@ -9,6 +9,7 @@ import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
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 { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
@@ -32,6 +33,7 @@ export class WorkspaceManagerService {
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly workspaceMigrationService: WorkspaceMigrationService,
private readonly objectMetadataService: ObjectMetadataService,
private readonly objectMetadataServiceV2: ObjectMetadataServiceV2,
private readonly dataSourceService: DataSourceService,
private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService,
@InjectRepository(UserWorkspaceEntity)
@@ -136,32 +138,23 @@ export class WorkspaceManagerService {
});
}
// TODO investigate why some entities are not on cascade delete
// Are foreign keys correctly applied ?
public async delete(workspaceId: string): Promise<void> {
//TODO: delete all logs when #611 closed
this.logger.log(`Deleting workspace ${workspaceId} ...`);
await this.roleTargetsRepository.delete({
workspaceId,
});
this.logger.log(`workspace ${workspaceId} role targets deleted`);
await this.roleRepository.delete({
workspaceId,
});
this.logger.log(`workspace ${workspaceId} role deleted`);
await this.objectMetadataService.deleteObjectsMetadata(workspaceId);
this.logger.log(`workspace ${workspaceId} object metadata deleted`);
await this.objectMetadataServiceV2.deleteWorkspaceAllObjectMetadata({
workspaceId,
});
await this.workspaceMigrationService.deleteAllWithinWorkspace(workspaceId);
this.logger.log(`workspace ${workspaceId} migration deleted`);
await this.dataSourceService.delete(workspaceId);
this.logger.log(`workspace ${workspaceId} data source deleted`);
// Delete schema
await this.workspaceDataSourceService.deleteWorkspaceDBSchema(workspaceId);
this.logger.log(`workspace ${workspaceId} schema deleted`);
}
private async setupDefaultRoles(
@@ -103,6 +103,7 @@ export class DeleteFieldActionHandlerService extends WorkspaceMigrationRunnerAct
schemaName,
tableName,
columnNames: columnNamesToDrop,
cascade: true,
});
const enumOperations = collectEnumOperationsForField({
@@ -89,6 +89,7 @@ export class DeleteObjectActionHandlerService extends WorkspaceMigrationRunnerAc
queryRunner,
schemaName,
tableName,
cascade: true,
});
const objectFlatFieldMetadatas =
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
@@ -73,21 +73,26 @@ export abstract class BaseWorkspaceMigrationRunnerActionHandlerService<
| MetadataToFlatEntityMapsKey<TMetadataName>
>
> {
await Promise.all([
this.asyncMethodPerformanceMetricWrapper({
label: 'executeForMetadata',
method: async () => this.executeForMetadata(context),
}),
this.asyncMethodPerformanceMetricWrapper({
label: 'executeForWorkspaceSchema',
method: async () => this.executeForWorkspaceSchema(context),
}),
]);
try {
await Promise.all([
this.asyncMethodPerformanceMetricWrapper({
label: 'executeForMetadata',
method: async () => this.executeForMetadata(context),
}),
this.asyncMethodPerformanceMetricWrapper({
label: 'executeForWorkspaceSchema',
method: async () => this.executeForWorkspaceSchema(context),
}),
]);
return this.optimisticallyApplyActionOnAllFlatEntityMaps({
action: context.action,
allFlatEntityMaps: context.allFlatEntityMaps,
});
return this.optimisticallyApplyActionOnAllFlatEntityMaps({
action: context.action,
allFlatEntityMaps: context.allFlatEntityMaps,
});
} catch (error) {
this.logger.error(`${this.actionType} execution failed`, error);
throw error;
}
}
async rollback(
@@ -5,6 +5,7 @@ import { getAuthTokensFromLoginToken } from 'test/integration/graphql/utils/get-
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 { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.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';
@@ -118,4 +119,128 @@ describe('Successful user and workspace creation', () => {
workpsaceCustomApplication.id,
);
});
it('should delete workspace and related metadata entities when last user is deleted', async () => {
const { data } = await signUp({
input: {
email: `test-delete-5678@example.com`,
password: 'Test123!@#',
},
expectToFail: false,
});
createdUserAccessToken =
data.signUp.tokens.accessOrWorkspaceAgnosticToken.token;
const {
data: { signUpInNewWorkspace: signUpInNewWorkspaceData },
} = await signUpInNewWorkspace({
accessToken: createdUserAccessToken,
expectToFail: false,
});
const workspaceId = signUpInNewWorkspaceData.workspace.id;
const {
data: { getAuthTokensFromLoginToken: authTokensData },
} = await getAuthTokensFromLoginToken({
origin: signUpInNewWorkspaceData.workspace.workspaceUrls.subdomainUrl,
loginToken: signUpInNewWorkspaceData.loginToken.token,
expectToFail: false,
});
const newWorkspaceAccessToken =
authTokensData.tokens.accessOrWorkspaceAgnosticToken.token;
await activateWorkspace({
accessToken: newWorkspaceAccessToken,
displayName: 'Test Workspace for Deletion',
expectToFail: false,
});
await createOneObjectMetadata({
input: {
nameSingular: 'workspaceEviction',
namePlural: 'workspaceEvictions',
labelPlural: 'whatevers',
labelSingular: 'whatever',
isLabelSyncedWithName: false,
},
token: newWorkspaceAccessToken,
expectToFail: false,
});
const workspaceBeforeDeletion = await testDataSource.query(
'SELECT * FROM core.workspace WHERE id = $1',
[workspaceId],
);
expect(workspaceBeforeDeletion).toHaveLength(1);
const tablesToVerify = [
'dataSource',
'objectMetadata',
'fieldMetadata',
'indexMetadata',
'searchFieldMetadata',
'workspaceMigration',
'role',
'roleTargets',
'objectPermission',
'fieldPermission',
'permissionFlag',
'serverlessFunction',
'serverlessFunctionLayer',
'agent',
'agentHandoff',
'remoteServer',
'remoteTable',
'databaseEventTrigger',
'view',
'viewField',
'viewFilter',
'viewFilterGroup',
'viewGroup',
'viewSort',
'cronTrigger',
'routeTrigger',
];
let totalRecordsBefore = 0;
for (const table of tablesToVerify) {
const result = await testDataSource.query(
`SELECT COUNT(*) as count FROM core."${table}" WHERE "workspaceId" = $1`,
[workspaceId],
);
totalRecordsBefore += parseInt(result[0].count);
}
expect(totalRecordsBefore).toBeGreaterThan(0);
await deleteUser({
accessToken: createdUserAccessToken,
expectToFail: false,
});
createdUserAccessToken = undefined;
const workspaceAfterDeletion = await testDataSource.query(
'SELECT * FROM core.workspace WHERE id = $1',
[workspaceId],
);
expect(workspaceAfterDeletion).toHaveLength(0);
for (const table of tablesToVerify) {
const result = await testDataSource.query(
`SELECT COUNT(*) as count FROM core."${table}" WHERE "workspaceId" = $1`,
[workspaceId],
);
const count = parseInt(result[0].count);
expect({ count, table }).toEqual({ count: 0, table });
}
});
});
@@ -14,6 +14,7 @@ export const createOneObjectMetadata = async ({
input,
gqlFields,
expectToFail = false,
token,
}: PerformMetadataQueryParams<CreateOneObjectFactoryInput>): CommonResponseBody<{
createOneObject: ObjectMetadataDTO;
}> => {
@@ -22,7 +23,7 @@ export const createOneObjectMetadata = async ({
gqlFields,
});
const response = await makeMetadataAPIRequest(graphqlOperation);
const response = await makeMetadataAPIRequest(graphqlOperation, token);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
@@ -6,12 +6,15 @@ type GraphqlOperation = {
variables?: Record<string, unknown>;
};
export const makeMetadataAPIRequest = (graphqlOperation: GraphqlOperation) => {
export const makeMetadataAPIRequest = (
graphqlOperation: GraphqlOperation,
token: string = APPLE_JANE_ADMIN_ACCESS_TOKEN,
) => {
const client = request(`http://localhost:${APP_PORT}`);
return client
.post('/metadata')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.set('Authorization', `Bearer ${token}`)
.send({
query: print(graphqlOperation.query),
variables: graphqlOperation.variables || {},
@@ -2,4 +2,5 @@ export type PerformMetadataQueryParams<T> = {
input: T;
gqlFields?: string;
expectToFail?: boolean | null;
token?: string;
};