Fix Tasks/Notes created with null position (#9068)

Fixes https://github.com/twentyhq/twenty/issues/8810
Fixes https://github.com/twentyhq/twenty/issues/5268
Fixes https://github.com/twentyhq/twenty/issues/8971

- Fixing Task/Note creation not sending position during creation
- Adding a command to backfill position being null, using existing
backfill command.
- Removed unused backfill job.
- Updated workspace entities to set position non-nullable and set a
default value to make it non-required on the API
- Updated position factory to set a default position for all objects
having a POSITION field instead of only company/people
- Moved the try/catch in each resolver factory calling
GraphqlQueryRunnerException handler, makes more sense to call it in the
actual graphql-query-runner and removing some duplicate codes
- Adding validations for input in QueryRunnerArgs factories
- Allow sync-metadata to override and sync defaultValues for certain
field types (that can't be updated by users)
- Removing health-check from sync-metadata command during force mode to
improve performances
This commit is contained in:
Weiko 2024-12-16 14:45:54 +01:00 committed by GitHub
parent 2ceb1c87b3
commit 5a27491bb2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 556 additions and 513 deletions

View File

@ -31,7 +31,7 @@ export const useOpenCreateActivityDrawer = ({
const setHotkeyScope = useSetHotkeyScope();
const { createOneRecord: createOneActivity } = useCreateOneRecord<
Task | Note
(Task | Note) & { position: 'first' | 'last' }
>({
objectNameSingular: activityObjectNameSingular,
});
@ -74,6 +74,7 @@ export const useOpenCreateActivityDrawer = ({
const activity = await createOneActivity({
assigneeId: customAssignee?.id,
position: 'last',
});
if (targetableObjects.length > 0) {

View File

@ -1,10 +1,10 @@
import { Module } from '@nestjs/common';
import { DatabaseCommandModule } from 'src/database/commands/database-command.module';
import { WorkspaceHealthCommandModule } from 'src/engine/workspace-manager/workspace-health/commands/workspace-health-command.module';
import { WorkspaceCleanerModule } from 'src/engine/workspace-manager/workspace-cleaner/workspace-cleaner.module';
import { AppModule } from 'src/app.module';
import { WorkspaceMigrationRunnerCommandsModule } from 'src/engine/workspace-manager/workspace-migration-runner/commands/workspace-sync-metadata-commands.module';
import { DatabaseCommandModule } from 'src/database/commands/database-command.module';
import { WorkspaceCleanerModule } from 'src/engine/workspace-manager/workspace-cleaner/workspace-cleaner.module';
import { WorkspaceHealthCommandModule } from 'src/engine/workspace-manager/workspace-health/commands/workspace-health-command.module';
import { WorkspaceMigrationRunnerCommandsModule } from 'src/engine/workspace-manager/workspace-migration-runner/commands/workspace-migration-runner-commands.module';
import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module';
@Module({

View File

@ -10,6 +10,7 @@ import { ConfirmationQuestion } from 'src/database/commands/questions/confirmati
import { UpgradeTo0_32CommandModule } from 'src/database/commands/upgrade-version/0-32/0-32-upgrade-version.module';
import { UpgradeTo0_33CommandModule } from 'src/database/commands/upgrade-version/0-33/0-33-upgrade-version.module';
import { UpgradeTo0_34CommandModule } from 'src/database/commands/upgrade-version/0-34/0-34-upgrade-version.module';
import { UpgradeTo0_40CommandModule } from 'src/database/commands/upgrade-version/0-40/0-40-upgrade-version.module';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
@ -52,6 +53,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
UpgradeTo0_33CommandModule,
UpgradeTo0_34CommandModule,
FeatureFlagModule,
UpgradeTo0_40CommandModule,
],
providers: [
DataSeedWorkspaceCommand,

View File

@ -0,0 +1,36 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
import { BaseCommandOptions } from 'src/database/commands/base.command';
import { RecordPositionBackfillService } from 'src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Command({
name: 'migrate-0.40:backfill-record-position',
description: 'Backfill record position',
})
export class RecordPositionBackfillCommand extends ActiveWorkspacesCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
private readonly recordPositionBackfillService: RecordPositionBackfillService,
) {
super(workspaceRepository);
}
async executeActiveWorkspacesCommand(
_passedParam: string[],
options: BaseCommandOptions,
workspaceIds: string[],
): Promise<void> {
for (const workspaceId of workspaceIds) {
await this.recordPositionBackfillService.backfill(
workspaceId,
options.dryRun ?? false,
);
}
}
}

View File

@ -0,0 +1,46 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
import { BaseCommandOptions } from 'src/database/commands/base.command';
import { RecordPositionBackfillCommand } from 'src/database/commands/upgrade-version/0-40/0-40-record-position-backfill.command';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
@Command({
name: 'upgrade-0.40',
description: 'Upgrade to 0.40',
})
export class UpgradeTo0_40Command extends ActiveWorkspacesCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
private readonly recordPositionBackfillCommand: RecordPositionBackfillCommand,
private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
) {
super(workspaceRepository);
}
async executeActiveWorkspacesCommand(
passedParam: string[],
options: BaseCommandOptions,
workspaceIds: string[],
): Promise<void> {
await this.recordPositionBackfillCommand.executeActiveWorkspacesCommand(
passedParam,
options,
workspaceIds,
);
await this.syncWorkspaceMetadataCommand.executeActiveWorkspacesCommand(
passedParam,
{
...options,
force: true,
},
workspaceIds,
);
}
}

View File

@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { RecordPositionBackfillCommand } from 'src/database/commands/upgrade-version/0-40/0-40-record-position-backfill.command';
import { UpgradeTo0_40Command } from 'src/database/commands/upgrade-version/0-40/0-40-upgrade-version.command';
import { RecordPositionBackfillModule } from 'src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-module';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module';
@Module({
imports: [
TypeOrmModule.forFeature([Workspace], 'core'),
WorkspaceSyncMetadataCommandsModule,
RecordPositionBackfillModule,
],
providers: [UpgradeTo0_40Command, RecordPositionBackfillCommand],
})
export class UpgradeTo0_40CommandModule {}

View File

@ -18,6 +18,7 @@ import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/
import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service';
import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory';
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@ -56,77 +57,81 @@ export abstract class GraphqlQueryBaseResolverService<
args: Input,
options: WorkspaceQueryRunnerOptions,
operationName: WorkspaceResolverBuilderMethodNames,
): Promise<Response> {
const { authContext, objectMetadataItemWithFieldMaps } = options;
): Promise<Response | undefined> {
try {
const { authContext, objectMetadataItemWithFieldMaps } = options;
await this.validate(args, options);
await this.validate(args, options);
const hookedArgs =
await this.workspaceQueryHookService.executePreQueryHooks(
const hookedArgs =
await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItemWithFieldMaps.nameSingular,
operationName,
args,
);
const computedArgs = (await this.queryRunnerArgsFactory.create(
hookedArgs,
options,
ResolverArgsType[capitalize(operationName)],
)) as Input;
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
authContext.workspace.id,
);
const repository = dataSource.getRepository(
objectMetadataItemWithFieldMaps.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadataItemWithFieldMaps.fieldsByName,
options.objectMetadataMaps,
);
const selectedFields = graphqlFields(options.info);
const graphqlQuerySelectedFieldsResult =
graphqlQueryParser.parseSelectedFields(
objectMetadataItemWithFieldMaps,
selectedFields,
);
const graphqlQueryResolverExecutionArgs = {
args: computedArgs,
options,
dataSource,
repository,
graphqlQueryParser,
graphqlQuerySelectedFieldsResult,
};
const results = await this.resolve(graphqlQueryResolverExecutionArgs);
const resultWithGetters = await this.queryResultGettersFactory.create(
results,
objectMetadataItemWithFieldMaps,
authContext.workspace.id,
options.objectMetadataMaps,
);
const resultWithGettersArray = Array.isArray(resultWithGetters)
? resultWithGetters
: [resultWithGetters];
await this.workspaceQueryHookService.executePostQueryHooks(
authContext,
objectMetadataItemWithFieldMaps.nameSingular,
operationName,
args,
resultWithGettersArray,
);
const computedArgs = (await this.queryRunnerArgsFactory.create(
hookedArgs,
options,
ResolverArgsType[capitalize(operationName)],
)) as Input;
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
authContext.workspace.id,
);
const repository = dataSource.getRepository(
objectMetadataItemWithFieldMaps.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadataItemWithFieldMaps.fieldsByName,
options.objectMetadataMaps,
);
const selectedFields = graphqlFields(options.info);
const graphqlQuerySelectedFieldsResult =
graphqlQueryParser.parseSelectedFields(
objectMetadataItemWithFieldMaps,
selectedFields,
);
const graphqlQueryResolverExecutionArgs = {
args: computedArgs,
options,
dataSource,
repository,
graphqlQueryParser,
graphqlQuerySelectedFieldsResult,
};
const results = await this.resolve(graphqlQueryResolverExecutionArgs);
const resultWithGetters = await this.queryResultGettersFactory.create(
results,
objectMetadataItemWithFieldMaps,
authContext.workspace.id,
options.objectMetadataMaps,
);
const resultWithGettersArray = Array.isArray(resultWithGetters)
? resultWithGetters
: [resultWithGetters];
await this.workspaceQueryHookService.executePostQueryHooks(
authContext,
objectMetadataItemWithFieldMaps.nameSingular,
operationName,
resultWithGettersArray,
);
return resultWithGetters;
return resultWithGetters;
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error, options);
}
}
protected abstract resolve(

View File

@ -83,8 +83,6 @@ export class GraphqlQueryCreateOneResolverService extends GraphqlQueryBaseResolv
});
}
async;
async validate(
args: CreateOneResolverArgs<Partial<ObjectRecord>>,
options: WorkspaceQueryRunnerOptions,

View File

@ -45,7 +45,7 @@ export class RecordPositionQueryFactory {
objectMetadata: { isCustom: boolean; nameSingular: string },
dataSourceSchema: string,
): [RecordPositionQuery, RecordPositionQueryParams] {
const name = computeTableName(
const tableName = computeTableName(
objectMetadata.nameSingular,
objectMetadata.isCustom,
);
@ -54,17 +54,17 @@ export class RecordPositionQueryFactory {
case RecordPositionQueryType.FIND_BY_POSITION:
return this.buildFindByPositionQuery(
recordPositionQueryArgs satisfies FindByPositionQueryArgs,
name,
tableName,
dataSourceSchema,
);
case RecordPositionQueryType.FIND_MIN_POSITION:
return this.buildFindMinPositionQuery(name, dataSourceSchema);
return this.buildFindMinPositionQuery(tableName, dataSourceSchema);
case RecordPositionQueryType.FIND_MAX_POSITION:
return this.buildFindMaxPositionQuery(name, dataSourceSchema);
return this.buildFindMaxPositionQuery(tableName, dataSourceSchema);
case RecordPositionQueryType.UPDATE_POSITION:
return this.buildUpdatePositionQuery(
recordPositionQueryArgs satisfies UpdatePositionQueryArgs,
name,
tableName,
dataSourceSchema,
);
default:

View File

@ -1,56 +0,0 @@
import { Command, CommandRunner, Option } from 'nest-commander';
import {
RecordPositionBackfillJob,
RecordPositionBackfillJobData,
} from 'src/engine/api/graphql/workspace-query-runner/jobs/record-position-backfill.job';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
export type RecordPositionBackfillCommandOptions = {
workspaceId: string;
dryRun?: boolean;
};
@Command({
name: 'migrate-0.20:backfill-record-position',
description: 'Backfill record position',
})
export class RecordPositionBackfillCommand extends CommandRunner {
constructor(
@InjectMessageQueue(MessageQueue.recordPositionBackfillQueue)
private readonly messageQueueService: MessageQueueService,
) {
super();
}
@Option({
flags: '-w, --workspace-id [workspace_id]',
description: 'workspace id',
required: true,
})
parseWorkspaceId(value: string): string {
return value;
}
@Option({
flags: '-d, --dry-run [dry run]',
description: 'Dry run: Log backfill actions.',
required: false,
})
dryRun(value: string): boolean {
return Boolean(value);
}
async run(
_passedParam: string[],
options: RecordPositionBackfillCommandOptions,
): Promise<void> {
this.messageQueueService.add<RecordPositionBackfillJobData>(
RecordPositionBackfillJob.name,
{ workspaceId: options.workspaceId, dryRun: options.dryRun ?? false },
{ retryLimit: 3 },
);
}
}

View File

@ -18,6 +18,23 @@ describe('QueryRunnerArgsFactory', () => {
objectMetadataItemWithFieldMaps: {
isCustom: true,
nameSingular: 'testNumber',
fields: [
{
type: FieldMetadataType.POSITION,
isCustom: true,
nameSingular: 'position',
},
{
type: FieldMetadataType.NUMBER,
isCustom: true,
nameSingular: 'testNumber',
},
{
type: FieldMetadataType.TEXT,
isCustom: true,
nameSingular: 'otherField',
},
],
fieldsByName: {
position: {
type: FieldMetadataType.POSITION,

View File

@ -17,7 +17,6 @@ import {
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { hasPositionField } from 'src/engine/metadata-modules/object-metadata/utils/has-position-field.util';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { RecordPositionFactory } from './record-position.factory';
@ -39,9 +38,10 @@ export class QueryRunnerArgsFactory {
const fieldMetadataMapByNameByName =
options.objectMetadataItemWithFieldMaps.fieldsByName;
const shouldBackfillPosition = hasPositionField(
options.objectMetadataItemWithFieldMaps,
);
const shouldBackfillPosition =
options.objectMetadataItemWithFieldMaps.fields.some(
(field) => field.type === FieldMetadataType.POSITION,
);
switch (resolverArgsType) {
case ResolverArgsType.CreateOne:

View File

@ -2,12 +2,12 @@ import { Injectable } from '@nestjs/common';
import { isDefined } from 'class-validator';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import {
RecordPositionQueryArgs,
RecordPositionQueryFactory,
RecordPositionQueryType,
} from 'src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
@Injectable()
export class RecordPositionFactory {

View File

@ -1,24 +0,0 @@
import { RecordPositionBackfillService } from 'src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-service';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
export type RecordPositionBackfillJobData = {
workspaceId: string;
dryRun: boolean;
};
@Processor(MessageQueue.recordPositionBackfillQueue)
export class RecordPositionBackfillJob {
constructor(
private readonly recordPositionBackfillService: RecordPositionBackfillService,
) {}
@Process(RecordPositionBackfillJob.name)
async handle(data: RecordPositionBackfillJobData): Promise<void> {
await this.recordPositionBackfillService.backfill(
data.workspaceId,
data.dryRun,
);
}
}

View File

@ -1,16 +0,0 @@
import { Module } from '@nestjs/common';
import { RecordPositionBackfillJob } from 'src/engine/api/graphql/workspace-query-runner/jobs/record-position-backfill.job';
import { RecordPositionBackfillModule } from 'src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
@Module({
imports: [
WorkspaceDataSourceModule,
DataSourceModule,
RecordPositionBackfillModule,
],
providers: [RecordPositionBackfillJob],
})
export class WorkspaceQueryRunnerJobModule {}

View File

@ -1,15 +1,17 @@
import { TestingModule, Test } from '@nestjs/testing';
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { RecordPositionQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory';
import { RecordPositionFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/record-position.factory';
import { RecordPositionBackfillService } from 'src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-service';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
describe('RecordPositionBackfillService', () => {
let recordPositionQueryFactory;
let recordPositionFactory;
let objectMetadataService;
let objectMetadataRepository;
let workspaceDataSourceService;
let service: RecordPositionBackfillService;
@ -27,8 +29,8 @@ describe('RecordPositionBackfillService', () => {
]),
};
objectMetadataService = {
findManyWithinWorkspace: jest.fn().mockReturnValue([]),
objectMetadataRepository = {
find: jest.fn().mockReturnValue([]),
};
workspaceDataSourceService = {
@ -51,8 +53,8 @@ describe('RecordPositionBackfillService', () => {
useValue: workspaceDataSourceService,
},
{
provide: ObjectMetadataService,
useValue: objectMetadataService,
provide: getRepositoryToken(ObjectMetadataEntity, 'metadata'),
useValue: objectMetadataRepository,
},
],
}).compile();
@ -76,23 +78,23 @@ describe('RecordPositionBackfillService', () => {
});
it('when objectMetadata without position, should do nothing', async () => {
objectMetadataService.findManyWithinWorkspace.mockReturnValue([
{
id: '1',
nameSingular: 'name',
fields: [],
},
]);
objectMetadataRepository.find.mockReturnValue([]);
await service.backfill('workspaceId', false);
expect(workspaceDataSourceService.executeRawQuery).not.toHaveBeenCalled();
});
it('when objectMetadata but all record with position, should create and run query once', async () => {
objectMetadataService.findManyWithinWorkspace.mockReturnValue([
objectMetadataRepository.find.mockReturnValue([
{
id: '1',
nameSingular: 'company',
fields: [],
fields: [
{
type: FieldMetadataType.POSITION,
isCustom: true,
nameSingular: 'position',
},
],
},
]);
await service.backfill('workspaceId', false);
@ -100,11 +102,17 @@ describe('RecordPositionBackfillService', () => {
});
it('when record without position, should create and run query twice', async () => {
objectMetadataService.findManyWithinWorkspace.mockReturnValue([
objectMetadataRepository.find.mockReturnValue([
{
id: '1',
nameSingular: 'company',
fields: [],
fields: [
{
type: FieldMetadataType.POSITION,
isCustom: true,
nameSingular: 'position',
},
],
},
]);
workspaceDataSourceService.executeRawQuery.mockResolvedValueOnce([
@ -119,11 +127,17 @@ describe('RecordPositionBackfillService', () => {
});
it('when dryRun is true, should not update position', async () => {
objectMetadataService.findManyWithinWorkspace.mockReturnValue([
objectMetadataRepository.find.mockReturnValue([
{
id: '1',
nameSingular: 'company',
fields: [],
fields: [
{
type: FieldMetadataType.POSITION,
isCustom: true,
nameSingular: 'position',
},
],
},
]);
workspaceDataSourceService.executeRawQuery.mockResolvedValueOnce([

View File

@ -1,13 +1,17 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { RecordPositionQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory';
import { RecordPositionFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/record-position.factory';
import { RecordPositionBackfillService } from 'src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-service';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
@Module({
imports: [WorkspaceDataSourceModule, ObjectMetadataModule],
imports: [
WorkspaceDataSourceModule,
TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
],
providers: [
RecordPositionFactory,
RecordPositionQueryFactory,

View File

@ -1,21 +1,24 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'class-validator';
import { Repository } from 'typeorm';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import {
RecordPositionQueryFactory,
RecordPositionQueryType,
} from 'src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory';
import { RecordPositionFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/record-position.factory';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { hasPositionField } from 'src/engine/metadata-modules/object-metadata/utils/has-position-field.util';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
@Injectable()
export class RecordPositionBackfillService {
private readonly logger = new Logger(RecordPositionBackfillService.name);
constructor(
private readonly objectMetadataService: ObjectMetadataService,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly recordPositionFactory: RecordPositionFactory,
private readonly recordPositionQueryFactory: RecordPositionQueryFactory,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
@ -29,15 +32,20 @@ export class RecordPositionBackfillService {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const objectMetadataEntities =
await this.objectMetadataService.findManyWithinWorkspace(workspaceId, {
where: { isSystem: false },
});
const objectMetadataCollection = await this.objectMetadataRepository.find({
where: {
workspaceId,
fields: {
name: 'position',
type: FieldMetadataType.POSITION,
},
},
relations: {
fields: true,
},
});
const objectMetadataWithPosition =
objectMetadataEntities.filter(hasPositionField);
for (const objectMetadata of objectMetadataWithPosition) {
for (const objectMetadata of objectMetadataCollection) {
const [recordsWithoutPositionQuery, recordsWithoutPositionQueryParams] =
this.recordPositionQueryFactory.create(
{

View File

@ -1,6 +1,6 @@
import { QueryFailedError } from 'typeorm';
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import {
GraphqlQueryRunnerException,
@ -20,7 +20,7 @@ import {
export const workspaceQueryRunnerGraphqlApiExceptionHandler = (
error: Error,
context: WorkspaceSchemaBuilderContext,
context: WorkspaceQueryRunnerOptions,
) => {
if (error instanceof QueryFailedError) {
if (
@ -96,6 +96,7 @@ export const workspaceQueryRunnerGraphqlApiExceptionHandler = (
case GraphqlQueryRunnerExceptionCode.UNSUPPORTED_OPERATOR:
case GraphqlQueryRunnerExceptionCode.ARGS_CONFLICT:
case GraphqlQueryRunnerExceptionCode.FIELD_NOT_FOUND:
case GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT:
throw new UserInputError(error.message);
case GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND:
throw new NotFoundError(error.message);

View File

@ -2,7 +2,6 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkspaceQueryBuilderModule } from 'src/engine/api/graphql/workspace-query-builder/workspace-query-builder.module';
import { RecordPositionBackfillCommand } from 'src/engine/api/graphql/workspace-query-runner/commands/0-20-record-position-backfill.command';
import { workspaceQueryRunnerFactories } from 'src/engine/api/graphql/workspace-query-runner/factories';
import { TelemetryListener } from 'src/engine/api/graphql/workspace-query-runner/listeners/telemetry.listener';
import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module';
@ -35,7 +34,6 @@ import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listen
...workspaceQueryRunnerFactories,
EntityEventsToDbListener,
TelemetryListener,
RecordPositionBackfillCommand,
],
exports: [...workspaceQueryRunnerFactories],
})

View File

@ -9,7 +9,6 @@ import {
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
@Injectable()
export class CreateManyResolverFactory
@ -27,23 +26,19 @@ export class CreateManyResolverFactory
const internalContext = context;
return async (_source, args, _context, info) => {
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.execute(
args,
options,
CreateManyResolverFactory.methodName,
);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error, context);
}
return await this.graphqlQueryRunnerService.execute(
args,
options,
CreateManyResolverFactory.methodName,
);
};
}
}

View File

@ -9,7 +9,6 @@ import {
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { GraphqlQueryCreateOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
@Injectable()
export class CreateOneResolverFactory
@ -27,23 +26,19 @@ export class CreateOneResolverFactory
const internalContext = context;
return async (_source, args, _context, info) => {
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.execute(
args,
options,
CreateOneResolverFactory.methodName,
);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error, internalContext);
}
return await this.graphqlQueryRunnerService.execute(
args,
options,
CreateOneResolverFactory.methodName,
);
};
}
}

View File

@ -9,7 +9,6 @@ import {
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { GraphqlQueryDeleteManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
@Injectable()
export class DeleteManyResolverFactory
@ -27,23 +26,19 @@ export class DeleteManyResolverFactory
const internalContext = context;
return async (_source, args, context, info) => {
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.execute(
args,
options,
DeleteManyResolverFactory.methodName,
);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error, internalContext);
}
return await this.graphqlQueryRunnerService.execute(
args,
options,
DeleteManyResolverFactory.methodName,
);
};
}
}

View File

@ -9,7 +9,6 @@ import {
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { GraphqlQueryDeleteOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
@Injectable()
export class DeleteOneResolverFactory
@ -27,23 +26,19 @@ export class DeleteOneResolverFactory
const internalContext = context;
return async (_source, args, context, info) => {
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.execute(
args,
options,
DeleteOneResolverFactory.methodName,
);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error, internalContext);
}
return await this.graphqlQueryRunnerService.execute(
args,
options,
DeleteOneResolverFactory.methodName,
);
};
}
}

View File

@ -9,7 +9,6 @@ import {
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { GraphqlQueryDestroyManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
@Injectable()
export class DestroyManyResolverFactory
@ -27,23 +26,19 @@ export class DestroyManyResolverFactory
const internalContext = context;
return async (_source, args, context, info) => {
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.execute(
args,
options,
DestroyManyResolverFactory.methodName,
);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error, internalContext);
}
return await this.graphqlQueryRunnerService.execute(
args,
options,
DestroyManyResolverFactory.methodName,
);
};
}
}

View File

@ -9,7 +9,6 @@ import {
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
@Injectable()
export class DestroyOneResolverFactory
@ -27,23 +26,19 @@ export class DestroyOneResolverFactory
const internalContext = context;
return async (_source, args, context, info) => {
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphQLQueryRunnerService.execute(
args,
options,
DestroyOneResolverFactory.methodName,
);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error, internalContext);
}
return await this.graphQLQueryRunnerService.execute(
args,
options,
DestroyOneResolverFactory.methodName,
);
};
}
}

View File

@ -9,7 +9,6 @@ import {
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { GraphqlQueryFindDuplicatesResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
@Injectable()
export class FindDuplicatesResolverFactory
@ -27,23 +26,19 @@ export class FindDuplicatesResolverFactory
const internalContext = context;
return async (_source, args, context, info) => {
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.execute(
args,
options,
FindDuplicatesResolverFactory.methodName,
);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error, internalContext);
}
return await this.graphqlQueryRunnerService.execute(
args,
options,
FindDuplicatesResolverFactory.methodName,
);
};
}
}

View File

@ -9,7 +9,6 @@ import {
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
@Injectable()
export class FindManyResolverFactory
@ -27,23 +26,19 @@ export class FindManyResolverFactory
const internalContext = context;
return async (_source, args, _context, info) => {
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.execute(
args,
options,
FindManyResolverFactory.methodName,
);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error, internalContext);
}
return await this.graphqlQueryRunnerService.execute(
args,
options,
FindManyResolverFactory.methodName,
);
};
}
}

View File

@ -9,7 +9,6 @@ import {
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
@Injectable()
export class FindOneResolverFactory
@ -27,23 +26,19 @@ export class FindOneResolverFactory
const internalContext = context;
return async (_source, args, _context, info) => {
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.execute(
args,
options,
FindOneResolverFactory.methodName,
);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error, internalContext);
}
return await this.graphqlQueryRunnerService.execute(
args,
options,
FindOneResolverFactory.methodName,
);
};
}
}

View File

@ -9,7 +9,6 @@ import {
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { GraphqlQueryRestoreManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
@Injectable()
export class RestoreManyResolverFactory
@ -27,23 +26,19 @@ export class RestoreManyResolverFactory
const internalContext = context;
return async (_source, args, context, info) => {
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.execute(
args,
options,
RestoreManyResolverFactory.methodName,
);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error, internalContext);
}
return await this.graphqlQueryRunnerService.execute(
args,
options,
RestoreManyResolverFactory.methodName,
);
};
}
}

View File

@ -9,7 +9,6 @@ import {
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { GraphqlQueryRestoreOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
@Injectable()
export class RestoreOneResolverFactory
@ -27,23 +26,19 @@ export class RestoreOneResolverFactory
const internalContext = context;
return async (_source, args, context, info) => {
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.execute(
args,
options,
RestoreOneResolverFactory.methodName,
);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error, internalContext);
}
return await this.graphqlQueryRunnerService.execute(
args,
options,
RestoreOneResolverFactory.methodName,
);
};
}
}

View File

@ -9,7 +9,6 @@ import {
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { GraphqlQuerySearchResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
@Injectable()
export class SearchResolverFactory
@ -25,23 +24,19 @@ export class SearchResolverFactory
const internalContext = context;
return async (_source, args, _context, info) => {
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.execute(
args,
options,
SearchResolverFactory.methodName,
);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error, internalContext);
}
return await this.graphqlQueryRunnerService.execute(
args,
options,
SearchResolverFactory.methodName,
);
};
}
}

View File

@ -9,7 +9,6 @@ import {
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { GraphqlQueryUpdateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
@Injectable()
export class UpdateManyResolverFactory
@ -27,23 +26,19 @@ export class UpdateManyResolverFactory
const internalContext = context;
return async (_source, args, context, info) => {
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.execute(
args,
options,
UpdateManyResolverFactory.methodName,
);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error, internalContext);
}
return await this.graphqlQueryRunnerService.execute(
args,
options,
UpdateManyResolverFactory.methodName,
);
};
}
}

View File

@ -9,7 +9,6 @@ import {
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { GraphqlQueryUpdateOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
@Injectable()
export class UpdateOneResolverFactory
@ -27,23 +26,19 @@ export class UpdateOneResolverFactory
const internalContext = context;
return async (_source, args, context, info) => {
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.execute(
args,
options,
UpdateOneResolverFactory.methodName,
);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error, internalContext);
}
return await this.graphqlQueryRunnerService.execute(
args,
options,
UpdateOneResolverFactory.methodName,
);
};
}
}

View File

@ -1,11 +1,16 @@
import { Logger } from '@nestjs/common/services/logger.service';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'class-validator';
import { Repository } from 'typeorm';
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { CreateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { buildCreatedByFromWorkspaceMember } from 'src/engine/core-modules/actor/utils/build-created-by-from-workspace-member.util';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
@ -45,6 +50,13 @@ export class CreatedByCreateManyPreQueryHook
): Promise<CreateManyResolverArgs<CustomWorkspaceItem>> {
let createdBy: ActorMetadata | null = null;
if (!isDefined(payload.data)) {
throw new GraphqlQueryRunnerException(
'Payload data is required',
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
// TODO: Once all objects have it, we can remove this check
const createdByFieldMetadata = await this.fieldMetadataRepository.findOne({
where: {

View File

@ -1,11 +1,16 @@
import { Logger } from '@nestjs/common/services/logger.service';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'class-validator';
import { Repository } from 'typeorm';
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { CreateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { buildCreatedByFromWorkspaceMember } from 'src/engine/core-modules/actor/utils/build-created-by-from-workspace-member.util';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
@ -45,6 +50,13 @@ export class CreatedByCreateOnePreQueryHook
): Promise<CreateOneResolverArgs<CustomWorkspaceItem>> {
let createdBy: ActorMetadata | null = null;
if (!isDefined(payload.data)) {
throw new GraphqlQueryRunnerException(
'Payload data is required',
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
// TODO: Once all objects have it, we can remove this check
const createdByFieldMetadata = await this.fieldMetadataRepository.findOne({
where: {

View File

@ -5,7 +5,6 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { DataSeedDemoWorkspaceModule } from 'src/database/commands/data-seed-demo-workspace/data-seed-demo-workspace.module';
import { DataSeedDemoWorkspaceJob } from 'src/database/commands/data-seed-demo-workspace/jobs/data-seed-demo-workspace.job';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { WorkspaceQueryRunnerJobModule } from 'src/engine/api/graphql/workspace-query-runner/jobs/workspace-query-runner-job.module';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
import { UpdateSubscriptionQuantityJob } from 'src/engine/core-modules/billing/jobs/update-subscription-quantity.job';
@ -47,7 +46,6 @@ import { WorkflowModule } from 'src/modules/workflow/workflow.module';
CalendarEventParticipantManagerModule,
TimelineActivityModule,
StripeModule,
WorkspaceQueryRunnerJobModule,
AutoCompaniesAndContactsCreationJobModule,
TimelineJobModule,
WebhookJobModule,

View File

@ -1,6 +0,0 @@
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
export const hasPositionField = (objectMetadataItem: ObjectMetadataInterface) =>
['opportunity', 'person', 'company'].includes(
objectMetadataItem.nameSingular,
) || objectMetadataItem.isCustom;

View File

@ -52,10 +52,10 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
description: 'Position',
type: FieldMetadataType.POSITION,
icon: 'IconHierarchy2',
defaultValue: 0,
})
@WorkspaceIsNullable()
@WorkspaceIsSystem()
position: number | null;
position: number;
@WorkspaceField({
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.createdBy,

View File

@ -50,40 +50,43 @@ export class SyncWorkspaceMetadataCommand extends ActiveWorkspacesCommandRunner
`Running workspace sync for workspace: ${workspaceId} (${count} out of ${workspaceIds.length})`,
);
count++;
try {
const issues =
await this.workspaceHealthService.healthCheck(workspaceId);
// Security: abort if there are issues.
if (issues.length > 0) {
if (!options.force) {
try {
const issues =
await this.workspaceHealthService.healthCheck(workspaceId);
// Security: abort if there are issues.
if (issues.length > 0) {
if (!options.force) {
this.logger.error(
`Workspace contains ${issues.length} issues, aborting.`,
);
this.logger.log(
'If you want to force the migration, use --force flag',
);
this.logger.log(
'Please use `workspace:health` command to check issues and fix them before running this command.',
);
continue;
}
this.logger.warn(
`Workspace contains ${issues.length} issues, sync has been forced.`,
);
}
} catch (error) {
if (!options.force) {
this.logger.error(
`Workspace contains ${issues.length} issues, aborting.`,
);
this.logger.log(
'If you want to force the migration, use --force flag',
);
this.logger.log(
'Please use `workspace:health` command to check issues and fix them before running this command.',
);
continue;
throw error;
}
this.logger.warn(
`Workspace contains ${issues.length} issues, sync has been forced.`,
`Workspace health check failed with error, but sync has been forced.`,
error,
);
}
} catch (error) {
if (!options.force) {
throw error;
}
this.logger.warn(
`Workspace health check failed with error, but sync has been forced.`,
error,
);
}
try {

View File

@ -8,7 +8,10 @@ import {
} from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface';
import { ComputedPartialFieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import {
FieldMetadataEntity,
FieldMetadataType,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
const commonFieldPropertiesToIgnore = [
@ -24,10 +27,24 @@ const commonFieldPropertiesToIgnore = [
'asExpression',
'generatedType',
'defaultValue',
'isLabelSyncedWithName',
];
const fieldPropertiesToStringify = ['defaultValue'] as const;
const shouldNotOverrideDefaultValue = (
fieldMetadata: FieldMetadataEntity | ComputedPartialFieldMetadata,
) => {
return [
FieldMetadataType.BOOLEAN,
FieldMetadataType.SELECT,
FieldMetadataType.MULTI_SELECT,
FieldMetadataType.CURRENCY,
FieldMetadataType.PHONES,
FieldMetadataType.ADDRESS,
].includes(fieldMetadata.type);
};
const shouldSkipFieldCreation = (
standardFieldMetadata: ComputedPartialFieldMetadata | undefined,
) => {
@ -55,7 +72,17 @@ export class WorkspaceFieldComparator {
const originalFieldMetadataMap = transformMetadataForComparison(
filteredOriginalFieldCollection,
{
shouldIgnoreProperty: (property) => {
shouldIgnoreProperty: (
property,
fieldMetadata: FieldMetadataEntity,
) => {
if (
property === 'defaultValue' &&
shouldNotOverrideDefaultValue(fieldMetadata)
) {
return true;
}
if (commonFieldPropertiesToIgnore.includes(property)) {
return true;
}
@ -72,7 +99,17 @@ export class WorkspaceFieldComparator {
const standardFieldMetadataMap = transformMetadataForComparison(
standardFieldMetadataCollection,
{
shouldIgnoreProperty: (property) => {
shouldIgnoreProperty: (
property,
fieldMetadata: ComputedPartialFieldMetadata,
) => {
if (
property === 'defaultValue' &&
shouldNotOverrideDefaultValue(fieldMetadata)
) {
return true;
}
if (commonFieldPropertiesToIgnore.includes(property)) {
return true;
}

View File

@ -147,9 +147,9 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
label: 'Position',
description: 'Company record position',
icon: 'IconHierarchy2',
defaultValue: 0,
})
@WorkspaceIsSystem()
@WorkspaceIsNullable()
position: number;
@WorkspaceField({

View File

@ -37,6 +37,7 @@ export class FavoriteFolderWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconList',
defaultValue: 0,
})
@WorkspaceIsSystem()
position: number;
@WorkspaceField({

View File

@ -48,6 +48,7 @@ export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconList',
defaultValue: 0,
})
@WorkspaceIsSystem()
position: number;
// Relations

View File

@ -55,10 +55,10 @@ export class NoteWorkspaceEntity extends BaseWorkspaceEntity {
label: 'Position',
description: 'Note record position',
icon: 'IconHierarchy2',
defaultValue: 0,
})
@WorkspaceIsSystem()
@WorkspaceIsNullable()
position: number | null;
position: number;
@WorkspaceField({
standardId: NOTE_STANDARD_FIELD_IDS.title,

View File

@ -113,10 +113,10 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity {
label: 'Position',
description: 'Opportunity record position',
icon: 'IconHierarchy2',
defaultValue: 0,
})
@WorkspaceIsSystem()
@WorkspaceIsNullable()
position: number | null;
position: number;
@WorkspaceField({
standardId: OPPORTUNITY_STANDARD_FIELD_IDS.createdBy,

View File

@ -157,9 +157,9 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
label: 'Position',
description: 'Person record Position',
icon: 'IconHierarchy2',
defaultValue: 0,
})
@WorkspaceIsSystem()
@WorkspaceIsNullable()
position: number;
@WorkspaceField({

View File

@ -57,10 +57,10 @@ export class TaskWorkspaceEntity extends BaseWorkspaceEntity {
label: 'Position',
description: 'Task record position',
icon: 'IconHierarchy2',
defaultValue: 0,
})
@WorkspaceIsSystem()
@WorkspaceIsNullable()
position: number | null;
position: number;
@WorkspaceField({
standardId: TASK_STANDARD_FIELD_IDS.title,

View File

@ -77,6 +77,7 @@ export class ViewFieldWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconList',
defaultValue: 0,
})
@WorkspaceIsSystem()
position: number;
@WorkspaceRelation({

View File

@ -59,6 +59,7 @@ export class ViewGroupWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconList',
defaultValue: 0,
})
@WorkspaceIsSystem()
position: number;
@WorkspaceRelation({

View File

@ -97,8 +97,9 @@ export class ViewWorkspaceEntity extends BaseWorkspaceEntity {
type: FieldMetadataType.POSITION,
label: 'Position',
description: 'View position',
defaultValue: 0,
})
@WorkspaceIsNullable()
@WorkspaceIsSystem()
position: number;
@WorkspaceField({

View File

@ -155,10 +155,10 @@ export class WorkflowRunWorkspaceEntity extends BaseWorkspaceEntity {
label: 'Position',
description: 'Workflow run position',
icon: 'IconHierarchy2',
defaultValue: 0,
})
@WorkspaceIsSystem()
@WorkspaceIsNullable()
position: number | null;
position: number;
// Relations
@WorkspaceRelation({

View File

@ -117,10 +117,10 @@ export class WorkflowVersionWorkspaceEntity extends BaseWorkspaceEntity {
label: 'Position',
description: 'Workflow version position',
icon: 'IconHierarchy2',
defaultValue: 0,
})
@WorkspaceIsSystem()
@WorkspaceIsNullable()
position: number | null;
position: number;
// Relations
@WorkspaceRelation({

View File

@ -1,6 +1,10 @@
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import {
ActorMetadata,
FieldActorSource,
} from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import {
RelationMetadataType,
@ -21,10 +25,6 @@ import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-o
import { WorkflowEventListenerWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-event-listener.workspace-entity';
import { WorkflowRunWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import {
ActorMetadata,
FieldActorSource,
} from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
export enum WorkflowStatus {
DRAFT = 'DRAFT',
@ -103,10 +103,10 @@ export class WorkflowWorkspaceEntity extends BaseWorkspaceEntity {
label: 'Position',
description: 'Workflow record position',
icon: 'IconHierarchy2',
defaultValue: 0,
})
@WorkspaceIsSystem()
@WorkspaceIsNullable()
position: number | null;
position: number;
// Relations
@WorkspaceRelation({