Refactor graphql query runner and add mutation resolvers (#7418)

Fixes https://github.com/twentyhq/twenty/issues/6859

This PR adds all the remaining resolvers for
- updateOne/updateMany
- createOne/createMany
- deleteOne/deleteMany
- destroyOne
- restoreMany

Also
- refactored the graphql-query-runner to be able to add other resolvers
without too much boilerplate.
- add missing events that were not sent anymore as well as webhooks
- make resolver injectable so they can inject other services as well
- use objectMetadataMap from cache instead of computing it multiple time
- various fixes (mutation not correctly parsing JSON, relationHelper
fetching data with empty ids set, ...)

Next steps: 
- Wrapping query builder to handle DB events properly
- Move webhook emitters to db event listener
- Add pagination where it's missing (findDuplicates, nested relations,
etc...)
This commit is contained in:
Weiko
2024-10-04 11:58:33 +02:00
committed by GitHub
parent 8afa504b65
commit 511150a2d3
43 changed files with 1696 additions and 775 deletions

View File

@@ -5,12 +5,12 @@ export class AddIndexType1725893697807 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TYPE metadata."indextype_enum" AS ENUM ('BTREE', 'GIN')`,
`CREATE TYPE "metadata"."indexMetadata_indextype_enum" AS ENUM('BTREE', 'GIN')`,
);
await queryRunner.query(`
ALTER TABLE metadata."indexMetadata"
ADD COLUMN "indexType" metadata."indextype_enum" NOT NULL DEFAULT 'BTREE';
ADD COLUMN "indexType" metadata."indexMetadata_indextype_enum" NOT NULL DEFAULT 'BTREE';
`);
}
@@ -19,6 +19,8 @@ export class AddIndexType1725893697807 implements MigrationInterface {
ALTER TABLE metadata."indexMetadata" DROP COLUMN "indexType"
`);
await queryRunner.query(`DROP TYPE metadata."indextype_enum"`);
await queryRunner.query(
`DROP TYPE metadata."indexMetadata_indextype_enum"`,
);
}
}

View File

@@ -0,0 +1,51 @@
import { Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
import {
ResolverArgs,
WorkspaceResolverBuilderMethodNames,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service';
import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service';
import { GraphqlQueryFindDuplicatesResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service';
import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service';
import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service';
import { GraphqlQuerySearchResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service';
import { GraphqlQueryUpdateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service';
import { GraphqlQueryUpdateOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service';
@Injectable()
export class GraphqlQueryResolverFactory {
constructor(private moduleRef: ModuleRef) {}
public getResolver(
operationName: WorkspaceResolverBuilderMethodNames,
): ResolverService<ResolverArgs, any> {
switch (operationName) {
case 'findOne':
return this.moduleRef.get(GraphqlQueryFindOneResolverService);
case 'findMany':
return this.moduleRef.get(GraphqlQueryFindManyResolverService);
case 'findDuplicates':
return this.moduleRef.get(GraphqlQueryFindDuplicatesResolverService);
case 'search':
return this.moduleRef.get(GraphqlQuerySearchResolverService);
case 'createOne':
case 'createMany':
return this.moduleRef.get(GraphqlQueryCreateManyResolverService);
case 'destroyOne':
return this.moduleRef.get(GraphqlQueryDestroyOneResolverService);
case 'updateOne':
case 'deleteOne':
return this.moduleRef.get(GraphqlQueryUpdateOneResolverService);
case 'updateMany':
case 'deleteMany':
case 'restoreMany':
return this.moduleRef.get(GraphqlQueryUpdateManyResolverService);
default:
throw new Error(`Unsupported operation: ${operationName}`);
}
}
}

View File

@@ -25,7 +25,7 @@ export class GraphqlQueryFilterConditionParser {
public parse(
queryBuilder: SelectQueryBuilder<any>,
objectNameSingular: string,
filter: RecordFilter,
filter: Partial<RecordFilter>,
): SelectQueryBuilder<any> {
if (!filter || Object.keys(filter).length === 0) {
return queryBuilder;

View File

@@ -58,7 +58,6 @@ export class GraphqlQueryFilterFieldParser {
}
const { sql, params } = this.computeWhereConditionParts(
fieldMetadata,
operator,
objectNameSingular,
key,
@@ -73,7 +72,6 @@ export class GraphqlQueryFilterFieldParser {
}
private computeWhereConditionParts(
fieldMetadata: FieldMetadataInterface,
operator: string,
objectNameSingular: string,
key: string,
@@ -185,7 +183,6 @@ export class GraphqlQueryFilterFieldParser {
);
const { sql, params } = this.computeWhereConditionParts(
fieldMetadata,
operator,
objectNameSingular,
fullFieldName,

View File

@@ -9,7 +9,6 @@ import {
RecordFilter,
RecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { GraphqlQueryFilterConditionParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser';
import { GraphqlQueryOrderFieldParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser';
@@ -17,6 +16,7 @@ import { GraphqlQuerySelectedFieldsParser } from 'src/engine/api/graphql/graphql
import {
FieldMetadataMap,
ObjectMetadataMap,
ObjectMetadataMapItem,
} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
export class GraphqlQueryParser {
@@ -39,10 +39,10 @@ export class GraphqlQueryParser {
);
}
applyFilterToBuilder(
public applyFilterToBuilder(
queryBuilder: SelectQueryBuilder<any>,
objectNameSingular: string,
recordFilter: RecordFilter,
recordFilter: Partial<RecordFilter>,
): SelectQueryBuilder<any> {
return this.filterConditionParser.parse(
queryBuilder,
@@ -51,7 +51,7 @@ export class GraphqlQueryParser {
);
}
applyDeletedAtToBuilder(
public applyDeletedAtToBuilder(
queryBuilder: SelectQueryBuilder<any>,
recordFilter: RecordFilter,
): SelectQueryBuilder<any> {
@@ -88,7 +88,7 @@ export class GraphqlQueryParser {
return false;
};
applyOrderToBuilder(
public applyOrderToBuilder(
queryBuilder: SelectQueryBuilder<any>,
orderBy: RecordOrderBy,
objectNameSingular: string,
@@ -103,8 +103,8 @@ export class GraphqlQueryParser {
return queryBuilder.orderBy(parsedOrderBys as OrderByCondition);
}
parseSelectedFields(
parentObjectMetadata: ObjectMetadataInterface,
public parseSelectedFields(
parentObjectMetadata: ObjectMetadataMapItem,
graphqlSelectedFields: Partial<Record<string, any>>,
): { select: Record<string, any>; relations: Record<string, any> } {
const parentFields =

View File

@@ -1,16 +1,43 @@
import { Module } from '@nestjs/common';
import { GraphqlQueryResolverFactory } from 'src/engine/api/graphql/graphql-query-runner/factories/graphql-query-resolver.factory';
import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service';
import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service';
import { GraphqlQueryFindDuplicatesResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service';
import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service';
import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service';
import { GraphqlQuerySearchResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service';
import { GraphqlQueryUpdateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service';
import { GraphqlQueryUpdateOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service';
import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service';
import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module';
import { WorkspaceQueryRunnerModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
const graphqlQueryResolvers = [
GraphqlQueryFindOneResolverService,
GraphqlQueryFindManyResolverService,
GraphqlQueryFindDuplicatesResolverService,
GraphqlQueryCreateManyResolverService,
GraphqlQueryDestroyOneResolverService,
GraphqlQueryUpdateOneResolverService,
GraphqlQueryUpdateManyResolverService,
GraphqlQuerySearchResolverService,
];
@Module({
imports: [
WorkspaceQueryHookModule,
WorkspaceQueryRunnerModule,
FeatureFlagModule,
],
providers: [GraphqlQueryRunnerService],
providers: [
GraphqlQueryRunnerService,
GraphqlQueryResolverFactory,
ApiEventEmitterService,
...graphqlQueryResolvers,
],
exports: [GraphqlQueryRunnerService],
})
export class GraphqlQueryRunnerModule {}

View File

@@ -6,180 +6,90 @@ import {
RecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
import { IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import {
CreateManyResolverArgs,
CreateOneResolverArgs,
DeleteManyResolverArgs,
DeleteOneResolverArgs,
DestroyOneResolverArgs,
FindDuplicatesResolverArgs,
FindManyResolverArgs,
FindOneResolverArgs,
ResolverArgs,
ResolverArgsType,
RestoreManyResolverArgs,
SearchResolverArgs,
UpdateManyResolverArgs,
UpdateOneResolverArgs,
WorkspaceResolverBuilderMethodNames,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service';
import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service';
import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service';
import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service';
import { GraphqlQuerySearchResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service';
import { GraphqlQueryResolverFactory } from 'src/engine/api/graphql/graphql-query-runner/factories/graphql-query-resolver.factory';
import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service';
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
import {
CallWebhookJobsJob,
CallWebhookJobsJobData,
CallWebhookJobsJobOperation,
} from 'src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job';
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service';
import {
WorkspaceQueryRunnerException,
WorkspaceQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
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';
import { LogExecutionTime } from 'src/engine/decorators/observability/log-execution-time.decorator';
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
import { capitalize } from 'src/utils/capitalize';
@Injectable()
export class GraphqlQueryRunnerService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly featureFlagService: FeatureFlagService,
private readonly workspaceQueryHookService: WorkspaceQueryHookService,
private readonly queryRunnerArgsFactory: QueryRunnerArgsFactory,
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
@InjectMessageQueue(MessageQueue.webhookQueue)
private readonly messageQueueService: MessageQueueService,
private readonly graphqlQueryResolverFactory: GraphqlQueryResolverFactory,
private readonly apiEventEmitterService: ApiEventEmitterService,
) {}
/** QUERIES */
@LogExecutionTime()
async findOne<
ObjectRecord extends IRecord = IRecord,
Filter extends RecordFilter = RecordFilter,
>(
async findOne<ObjectRecord extends IRecord, Filter extends RecordFilter>(
args: FindOneResolverArgs<Filter>,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord | undefined> {
const graphqlQueryFindOneResolverService =
new GraphqlQueryFindOneResolverService(this.twentyORMGlobalManager);
const { authContext, objectMetadataItem } = options;
if (!args.filter || Object.keys(args.filter).length === 0) {
throw new WorkspaceQueryRunnerException(
'Missing filter argument',
WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
const hookedArgs =
await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItem.nameSingular,
'findOne',
args,
);
const computedArgs = (await this.queryRunnerArgsFactory.create(
hookedArgs,
): Promise<ObjectRecord> {
return this.executeQuery<FindOneResolverArgs<Filter>, ObjectRecord>(
'findOne',
args,
options,
ResolverArgsType.FindOne,
)) as FindOneResolverArgs<Filter>;
return graphqlQueryFindOneResolverService.findOne(computedArgs, options);
);
}
@LogExecutionTime()
async findMany<
ObjectRecord extends IRecord = IRecord,
Filter extends RecordFilter = RecordFilter,
OrderBy extends RecordOrderBy = RecordOrderBy,
ObjectRecord extends IRecord,
Filter extends RecordFilter,
OrderBy extends RecordOrderBy,
>(
args: FindManyResolverArgs<Filter, OrderBy>,
options: WorkspaceQueryRunnerOptions,
): Promise<IConnection<ObjectRecord>> {
const graphqlQueryFindManyResolverService =
new GraphqlQueryFindManyResolverService(this.twentyORMGlobalManager);
const { authContext, objectMetadataItem } = options;
const hookedArgs =
await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItem.nameSingular,
'findMany',
args,
);
const computedArgs = (await this.queryRunnerArgsFactory.create(
hookedArgs,
options,
ResolverArgsType.FindMany,
)) as FindManyResolverArgs<Filter, OrderBy>;
return graphqlQueryFindManyResolverService.findMany(computedArgs, options);
): Promise<IConnection<ObjectRecord, IEdge<ObjectRecord>>> {
return this.executeQuery<
FindManyResolverArgs<Filter, OrderBy>,
IConnection<ObjectRecord, IEdge<ObjectRecord>>
>('findMany', args, options);
}
@LogExecutionTime()
async createOne<ObjectRecord extends IRecord = IRecord>(
args: CreateOneResolverArgs<Partial<ObjectRecord>>,
async findDuplicates<ObjectRecord extends IRecord>(
args: FindDuplicatesResolverArgs<Partial<ObjectRecord>>,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord | undefined> {
const graphqlQueryCreateManyResolverService =
new GraphqlQueryCreateManyResolverService(this.twentyORMGlobalManager);
const { authContext, objectMetadataItem } = options;
assertMutationNotOnRemoteObject(objectMetadataItem);
if (args.data.id) {
assertIsValidUuid(args.data.id);
}
const createManyArgs = {
data: [args.data],
upsert: args.upsert,
} as CreateManyResolverArgs<ObjectRecord>;
const hookedArgs =
await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItem.nameSingular,
'createMany',
createManyArgs,
);
const computedArgs = (await this.queryRunnerArgsFactory.create(
hookedArgs,
options,
ResolverArgsType.CreateMany,
)) as CreateManyResolverArgs<ObjectRecord>;
const results = (await graphqlQueryCreateManyResolverService.createMany(
computedArgs,
options,
)) as ObjectRecord[];
await this.triggerWebhooks<ObjectRecord>(
results,
CallWebhookJobsJobOperation.create,
options,
);
this.emitCreateEvents<ObjectRecord>(
results,
authContext,
objectMetadataItem,
);
return results?.[0] as ObjectRecord;
): Promise<IConnection<ObjectRecord>[]> {
return this.executeQuery<
FindDuplicatesResolverArgs<Partial<ObjectRecord>>,
IConnection<ObjectRecord>[]
>('findDuplicates', args, options);
}
@LogExecutionTime()
@@ -187,104 +97,286 @@ export class GraphqlQueryRunnerService {
args: SearchResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<IConnection<ObjectRecord>> {
const graphqlQuerySearchResolverService =
new GraphqlQuerySearchResolverService(
this.twentyORMGlobalManager,
this.featureFlagService,
);
return this.executeQuery<SearchResolverArgs, IConnection<ObjectRecord>>(
'search',
args,
options,
);
}
return graphqlQuerySearchResolverService.search(args, options);
/** MUTATIONS */
@LogExecutionTime()
async createOne<ObjectRecord extends IRecord>(
args: CreateOneResolverArgs<Partial<ObjectRecord>>,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord> {
const results = await this.executeQuery<
CreateManyResolverArgs<Partial<ObjectRecord>>,
ObjectRecord[]
>('createMany', { data: [args.data], upsert: args.upsert }, options);
// TODO: emitCreateEvents should be moved to the ORM layer
if (results) {
this.apiEventEmitterService.emitCreateEvents(
results,
options.authContext,
options.objectMetadataItem,
);
}
return results[0];
}
@LogExecutionTime()
async createMany<ObjectRecord extends IRecord = IRecord>(
async createMany<ObjectRecord extends IRecord>(
args: CreateManyResolverArgs<Partial<ObjectRecord>>,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord[] | undefined> {
const graphqlQueryCreateManyResolverService =
new GraphqlQueryCreateManyResolverService(this.twentyORMGlobalManager);
): Promise<ObjectRecord[]> {
const results = await this.executeQuery<
CreateManyResolverArgs<Partial<ObjectRecord>>,
ObjectRecord[]
>('createMany', args, options);
if (results) {
this.apiEventEmitterService.emitCreateEvents(
results,
options.authContext,
options.objectMetadataItem,
);
}
return results;
}
@LogExecutionTime()
public async updateOne<ObjectRecord extends IRecord>(
args: UpdateOneResolverArgs<Partial<ObjectRecord>>,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord> {
const existingRecord = await this.executeQuery<
FindOneResolverArgs,
ObjectRecord
>(
'findOne',
{
filter: { id: { eq: args.id } },
},
options,
);
const result = await this.executeQuery<
UpdateOneResolverArgs<Partial<ObjectRecord>>,
ObjectRecord
>('updateOne', args, options);
this.apiEventEmitterService.emitUpdateEvents(
[existingRecord],
[result],
Object.keys(args.data),
options.authContext,
options.objectMetadataItem,
);
return result;
}
@LogExecutionTime()
public async updateMany<ObjectRecord extends IRecord>(
args: UpdateManyResolverArgs<Partial<ObjectRecord>>,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord[]> {
const existingRecords = await this.executeQuery<
FindManyResolverArgs,
IConnection<ObjectRecord, IEdge<ObjectRecord>>
>(
'findMany',
{
filter: args.filter,
},
options,
);
const result = await this.executeQuery<
UpdateManyResolverArgs<Partial<ObjectRecord>>,
ObjectRecord[]
>('updateMany', args, options);
this.apiEventEmitterService.emitUpdateEvents(
existingRecords.edges.map((edge) => edge.node),
result,
Object.keys(args.data),
options.authContext,
options.objectMetadataItem,
);
return result;
}
@LogExecutionTime()
public async deleteOne<ObjectRecord extends IRecord & { deletedAt?: Date }>(
args: DeleteOneResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord> {
const result = await this.executeQuery<
UpdateOneResolverArgs<Partial<ObjectRecord>>,
ObjectRecord
>(
'deleteOne',
{
id: args.id,
data: { deletedAt: new Date() } as Partial<ObjectRecord>,
},
options,
);
this.apiEventEmitterService.emitDeletedEvents(
[result],
options.authContext,
options.objectMetadataItem,
);
return result;
}
@LogExecutionTime()
public async deleteMany<ObjectRecord extends IRecord & { deletedAt?: Date }>(
args: DeleteManyResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord[]> {
const result = await this.executeQuery<
UpdateManyResolverArgs<Partial<ObjectRecord>>,
ObjectRecord[]
>(
'deleteMany',
{
filter: args.filter,
data: { deletedAt: new Date() } as Partial<ObjectRecord>,
},
options,
);
this.apiEventEmitterService.emitDeletedEvents(
result,
options.authContext,
options.objectMetadataItem,
);
return result;
}
@LogExecutionTime()
async destroyOne<ObjectRecord extends IRecord>(
args: DestroyOneResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord> {
const result = await this.executeQuery<
DestroyOneResolverArgs,
ObjectRecord
>('destroyOne', args, options);
this.apiEventEmitterService.emitDestroyEvents(
[result],
options.authContext,
options.objectMetadataItem,
);
return result;
}
@LogExecutionTime()
public async restoreMany<ObjectRecord extends IRecord>(
args: RestoreManyResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord> {
const result = await this.executeQuery<
UpdateManyResolverArgs<Partial<ObjectRecord>>,
ObjectRecord
>(
'restoreMany',
{
filter: args.filter,
data: { deletedAt: null } as Partial<ObjectRecord>,
},
options,
);
return result;
}
private async executeQuery<Input extends ResolverArgs, Response>(
operationName: WorkspaceResolverBuilderMethodNames,
args: Input,
options: WorkspaceQueryRunnerOptions,
): Promise<Response> {
const { authContext, objectMetadataItem } = options;
assertMutationNotOnRemoteObject(objectMetadataItem);
const resolver =
this.graphqlQueryResolverFactory.getResolver(operationName);
args.data.forEach((record) => {
if (record?.id) {
assertIsValidUuid(record.id);
}
});
await resolver.validate(args, options);
const hookedArgs =
await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItem.nameSingular,
'createMany',
operationName,
args,
);
const computedArgs = (await this.queryRunnerArgsFactory.create(
const computedArgs = await this.queryRunnerArgsFactory.create(
hookedArgs,
options,
ResolverArgsType.CreateMany,
)) as CreateManyResolverArgs<ObjectRecord>;
ResolverArgsType[capitalize(operationName)],
);
const results = (await graphqlQueryCreateManyResolverService.createMany(
computedArgs,
options,
)) as ObjectRecord[];
const results = await resolver.resolve(computedArgs as Input, options);
await this.workspaceQueryHookService.executePostQueryHooks(
authContext,
objectMetadataItem.nameSingular,
'createMany',
results,
operationName,
Array.isArray(results) ? results : [results],
);
await this.triggerWebhooks<ObjectRecord>(
results,
CallWebhookJobsJobOperation.create,
options,
);
const jobOperation = this.operationNameToJobOperation(operationName);
this.emitCreateEvents<ObjectRecord>(
results,
authContext,
objectMetadataItem,
);
if (jobOperation) {
await this.triggerWebhooks(results, jobOperation, options);
}
return results;
}
private emitCreateEvents<BaseRecord extends IRecord = IRecord>(
records: BaseRecord[],
authContext: AuthContext,
objectMetadataItem: ObjectMetadataInterface,
) {
this.workspaceEventEmitter.emit(
`${objectMetadataItem.nameSingular}.created`,
records.map(
(record) =>
({
userId: authContext.user?.id,
recordId: record.id,
objectMetadata: objectMetadataItem,
properties: {
after: record,
},
}) satisfies ObjectRecordCreateEvent<any>,
),
authContext.workspace.id,
);
private operationNameToJobOperation(
operationName: WorkspaceResolverBuilderMethodNames,
): CallWebhookJobsJobOperation | undefined {
switch (operationName) {
case 'createOne':
case 'createMany':
return CallWebhookJobsJobOperation.create;
case 'updateOne':
case 'updateMany':
case 'restoreMany':
return CallWebhookJobsJobOperation.update;
case 'deleteOne':
case 'deleteMany':
return CallWebhookJobsJobOperation.delete;
case 'destroyOne':
return CallWebhookJobsJobOperation.destroy;
default:
return undefined;
}
}
private async triggerWebhooks<Record>(
jobsData: Record[] | undefined,
private async triggerWebhooks<T>(
jobsData: T[] | undefined,
operation: CallWebhookJobsJobOperation,
options: WorkspaceQueryRunnerOptions,
) {
if (!Array.isArray(jobsData)) {
return;
}
): Promise<void> {
if (!jobsData || !Array.isArray(jobsData)) return;
jobsData.forEach((jobData) => {
this.messageQueueService.add<CallWebhookJobsJobData>(
CallWebhookJobsJob.name,
@@ -298,99 +390,4 @@ export class GraphqlQueryRunnerService {
);
});
}
@LogExecutionTime()
async destroyOne<ObjectRecord extends IRecord = IRecord>(
args: DestroyOneResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord | undefined> {
const graphqlQueryDestroyOneResolverService =
new GraphqlQueryDestroyOneResolverService(this.twentyORMGlobalManager);
const { authContext, objectMetadataItem } = options;
assertMutationNotOnRemoteObject(objectMetadataItem);
assertIsValidUuid(args.id);
const hookedArgs =
await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItem.nameSingular,
'destroyOne',
args,
);
const computedArgs = (await this.queryRunnerArgsFactory.create(
hookedArgs,
options,
ResolverArgsType.DestroyOne,
)) as DestroyOneResolverArgs;
const result = (await graphqlQueryDestroyOneResolverService.destroyOne(
computedArgs,
options,
)) as ObjectRecord;
await this.workspaceQueryHookService.executePostQueryHooks(
authContext,
objectMetadataItem.nameSingular,
'destroyOne',
[result],
);
await this.triggerWebhooks<IRecord>(
[result],
CallWebhookJobsJobOperation.destroy,
options,
);
this.emitDestroyEvents<IRecord>([result], authContext, objectMetadataItem);
return result;
}
private emitDestroyEvents<BaseRecord extends IRecord = IRecord>(
records: BaseRecord[],
authContext: AuthContext,
objectMetadataItem: ObjectMetadataInterface,
) {
this.workspaceEventEmitter.emit(
`${objectMetadataItem.nameSingular}.destroyed`,
records.map((record) => {
return {
userId: authContext.user?.id,
recordId: record.id,
objectMetadata: objectMetadataItem,
properties: {
before: this.removeNestedProperties(record),
},
} satisfies ObjectRecordDeleteEvent<any>;
}),
authContext.workspace.id,
);
}
private removeNestedProperties<Record extends IRecord = IRecord>(
record: Record,
) {
if (!record) {
return;
}
const sanitizedRecord = {};
for (const [key, value] of Object.entries(record)) {
if (value && typeof value === 'object' && value['edges']) {
continue;
}
if (key === '__typename') {
continue;
}
sanitizedRecord[key] = value;
}
return sanitizedRecord;
}
}

View File

@@ -20,7 +20,7 @@ import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspac
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { isPlainObject } from 'src/utils/is-plain-object';
export class ObjectRecordsToGraphqlConnectionMapper {
export class ObjectRecordsToGraphqlConnectionHelper {
private objectMetadataMap: ObjectMetadataMap;
constructor(objectMetadataMap: ObjectMetadataMap) {

View File

@@ -4,6 +4,7 @@ import {
FindOptionsRelations,
In,
ObjectLiteral,
Repository,
} from 'typeorm';
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
@@ -16,14 +17,70 @@ import {
ObjectMetadataMap,
ObjectMetadataMapItem,
} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { deduceRelationDirection } from 'src/engine/utils/deduce-relation-direction.util';
export class ProcessNestedRelationsHelper {
private readonly twentyORMGlobalManager: TwentyORMGlobalManager;
constructor() {}
constructor(twentyORMGlobalManager: TwentyORMGlobalManager) {
this.twentyORMGlobalManager = twentyORMGlobalManager;
public async processNestedRelations<ObjectRecord extends IRecord = IRecord>(
objectMetadataMap: ObjectMetadataMap,
parentObjectMetadataItem: ObjectMetadataMapItem,
parentObjectRecords: ObjectRecord[],
relations: Record<string, FindOptionsRelations<ObjectLiteral>>,
limit: number,
authContext: any,
dataSource: DataSource,
): Promise<void> {
const processRelationTasks = Object.entries(relations).map(
([relationName, nestedRelations]) =>
this.processRelation(
objectMetadataMap,
parentObjectMetadataItem,
parentObjectRecords,
relationName,
nestedRelations,
limit,
authContext,
dataSource,
),
);
await Promise.all(processRelationTasks);
}
private async processRelation<ObjectRecord extends IRecord = IRecord>(
objectMetadataMap: ObjectMetadataMap,
parentObjectMetadataItem: ObjectMetadataMapItem,
parentObjectRecords: ObjectRecord[],
relationName: string,
nestedRelations: any,
limit: number,
authContext: any,
dataSource: DataSource,
): Promise<void> {
const relationFieldMetadata = parentObjectMetadataItem.fields[relationName];
const relationMetadata = getRelationMetadata(relationFieldMetadata);
const relationDirection = deduceRelationDirection(
relationFieldMetadata,
relationMetadata,
);
const processor =
relationDirection === 'to'
? this.processToRelation
: this.processFromRelation;
await processor.call(
this,
objectMetadataMap,
parentObjectMetadataItem,
parentObjectRecords,
relationName,
nestedRelations,
limit,
authContext,
dataSource,
);
}
private async processFromRelation<ObjectRecord extends IRecord = IRecord>(
@@ -35,49 +92,36 @@ export class ProcessNestedRelationsHelper {
limit: number,
authContext: any,
dataSource: DataSource,
) {
const relationFieldMetadata = parentObjectMetadataItem.fields[relationName];
const relationMetadata = getRelationMetadata(relationFieldMetadata);
const inverseRelationName =
objectMetadataMap[relationMetadata.toObjectMetadataId]?.fields[
relationMetadata.toFieldMetadataId
]?.name;
const referenceObjectMetadata = getRelationObjectMetadata(
relationFieldMetadata,
objectMetadataMap,
);
const referenceObjectMetadataName = referenceObjectMetadata.nameSingular;
const relationRepository = await dataSource.getRepository(
referenceObjectMetadataName,
);
const relationIds = parentObjectRecords.map((item) => item.id);
const uniqueRelationIds = [...new Set(relationIds)];
const relationFindOptions: FindManyOptions = {
where: {
[`${inverseRelationName}Id`]: In(uniqueRelationIds),
},
take: limit * parentObjectRecords.length,
};
const relationResults = await relationRepository.find(relationFindOptions);
parentObjectRecords.forEach((item) => {
(item as any)[relationName] = relationResults.filter(
(rel) => rel[`${inverseRelationName}Id`] === item.id,
): Promise<void> {
const { inverseRelationName, referenceObjectMetadata } =
this.getRelationMetadata(
objectMetadataMap,
parentObjectMetadataItem,
relationName,
);
});
const relationRepository = dataSource.getRepository(
referenceObjectMetadata.nameSingular,
);
const relationIds = this.getUniqueIds(parentObjectRecords, 'id');
const relationResults = await this.findRelations(
relationRepository,
inverseRelationName,
relationIds,
limit * parentObjectRecords.length,
);
this.assignRelationResults(
parentObjectRecords,
relationResults,
relationName,
`${inverseRelationName}Id`,
);
if (Object.keys(nestedRelations).length > 0) {
await this.processNestedRelations(
objectMetadataMap,
objectMetadataMap[referenceObjectMetadataName],
objectMetadataMap[referenceObjectMetadata.nameSingular],
relationResults as ObjectRecord[],
nestedRelations as Record<string, FindOptionsRelations<ObjectLiteral>>,
limit,
@@ -96,48 +140,37 @@ export class ProcessNestedRelationsHelper {
limit: number,
authContext: any,
dataSource: DataSource,
) {
const relationFieldMetadata = parentObjectMetadataItem.fields[relationName];
const referenceObjectMetadata = getRelationObjectMetadata(
relationFieldMetadata,
): Promise<void> {
const { referenceObjectMetadata } = this.getRelationMetadata(
objectMetadataMap,
parentObjectMetadataItem,
relationName,
);
const referenceObjectMetadataName = referenceObjectMetadata.nameSingular;
const relationRepository = dataSource.getRepository(
referenceObjectMetadataName,
referenceObjectMetadata.nameSingular,
);
const relationIds = parentObjectRecords.map(
(item) => item[`${relationName}Id`],
const relationIds = this.getUniqueIds(
parentObjectRecords,
`${relationName}Id`,
);
const relationResults = await this.findRelations(
relationRepository,
'id',
relationIds,
limit,
);
const uniqueRelationIds = [...new Set(relationIds)];
const relationFindOptions: FindManyOptions = {
where: {
id: In(uniqueRelationIds),
},
take: limit,
};
const relationResults = await relationRepository.find(relationFindOptions);
parentObjectRecords.forEach((item) => {
if (relationResults.length === 0) {
(item as any)[`${relationName}Id`] = null;
}
(item as any)[relationName] = relationResults.filter(
(rel) => rel.id === item[`${relationName}Id`],
)[0];
});
this.assignToRelationResults(
parentObjectRecords,
relationResults,
relationName,
);
if (Object.keys(nestedRelations).length > 0) {
await this.processNestedRelations(
objectMetadataMap,
objectMetadataMap[referenceObjectMetadataName],
objectMetadataMap[referenceObjectMetadata.nameSingular],
relationResults as ObjectRecord[],
nestedRelations as Record<string, FindOptionsRelations<ObjectLiteral>>,
limit,
@@ -147,48 +180,71 @@ export class ProcessNestedRelationsHelper {
}
}
public async processNestedRelations<ObjectRecord extends IRecord = IRecord>(
private getRelationMetadata(
objectMetadataMap: ObjectMetadataMap,
parentObjectMetadataItem: ObjectMetadataMapItem,
parentObjectRecords: ObjectRecord[],
relations: Record<string, FindOptionsRelations<ObjectLiteral>>,
limit: number,
authContext: any,
dataSource: DataSource,
relationName: string,
) {
for (const [relationName, nestedRelations] of Object.entries(relations)) {
const relationFieldMetadata =
parentObjectMetadataItem.fields[relationName];
const relationMetadata = getRelationMetadata(relationFieldMetadata);
const relationFieldMetadata = parentObjectMetadataItem.fields[relationName];
const relationMetadata = getRelationMetadata(relationFieldMetadata);
const referenceObjectMetadata = getRelationObjectMetadata(
relationFieldMetadata,
objectMetadataMap,
);
const inverseRelationName =
objectMetadataMap[relationMetadata.toObjectMetadataId]?.fields[
relationMetadata.toFieldMetadataId
]?.name;
const relationDirection = deduceRelationDirection(
relationFieldMetadata,
relationMetadata,
);
return { inverseRelationName, referenceObjectMetadata };
}
if (relationDirection === 'to') {
await this.processToRelation(
objectMetadataMap,
parentObjectMetadataItem,
parentObjectRecords,
relationName,
nestedRelations,
limit,
authContext,
dataSource,
);
} else {
await this.processFromRelation(
objectMetadataMap,
parentObjectMetadataItem,
parentObjectRecords,
relationName,
nestedRelations,
limit,
authContext,
dataSource,
);
}
private getUniqueIds(records: IRecord[], idField: string): any[] {
return [...new Set(records.map((item) => item[idField]))];
}
private async findRelations(
repository: Repository<any>,
field: string,
ids: any[],
limit: number,
): Promise<any[]> {
if (ids.length === 0) {
return [];
}
const findOptions: FindManyOptions = {
where: { [field]: In(ids) },
take: limit,
};
return repository.find(findOptions);
}
private assignRelationResults(
parentRecords: IRecord[],
relationResults: any[],
relationName: string,
joinField: string,
): void {
parentRecords.forEach((item) => {
(item as any)[relationName] = relationResults.filter(
(rel) => rel[joinField] === item.id,
);
});
}
private assignToRelationResults(
parentRecords: IRecord[],
relationResults: any[],
relationName: string,
): void {
parentRecords.forEach((item) => {
if (relationResults.length === 0) {
(item as any)[`${relationName}Id`] = null;
}
(item as any)[relationName] =
relationResults.find((rel) => rel.id === item[`${relationName}Id`]) ??
null;
});
}
}

View File

@@ -0,0 +1,12 @@
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
export interface ResolverService<ResolverArgs, T> {
resolve: (
args: ResolverArgs,
options: WorkspaceQueryRunnerOptions,
) => Promise<T>;
validate: (
args: ResolverArgs,
options: WorkspaceQueryRunnerOptions,
) => Promise<void>;
}

View File

@@ -1,51 +1,53 @@
import { Injectable } from '@nestjs/common';
import graphqlFields from 'graphql-fields';
import { In, InsertResult } from 'typeorm';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { CreateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper';
import { getObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-object-metadata-or-throw.util';
import { generateObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
export class GraphqlQueryCreateManyResolverService {
private twentyORMGlobalManager: TwentyORMGlobalManager;
@Injectable()
export class GraphqlQueryCreateManyResolverService
implements ResolverService<CreateManyResolverArgs, IRecord[]>
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
constructor(twentyORMGlobalManager: TwentyORMGlobalManager) {
this.twentyORMGlobalManager = twentyORMGlobalManager;
}
async createMany<ObjectRecord extends IRecord = IRecord>(
async resolve<ObjectRecord extends IRecord = IRecord>(
args: CreateManyResolverArgs<Partial<ObjectRecord>>,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord[] | undefined> {
const { authContext, objectMetadataItem, objectMetadataCollection, info } =
): Promise<ObjectRecord[]> {
const { authContext, info, objectMetadataMap, objectMetadataMapItem } =
options;
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
authContext.workspace.id,
objectMetadataItem.nameSingular,
);
const repository = dataSource.getRepository(
objectMetadataMapItem.nameSingular,
);
const objectMetadataMap = generateObjectMetadataMap(
objectMetadataCollection,
);
const objectMetadata = getObjectMetadataOrThrow(
objectMetadataMap,
objectMetadataItem.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadata.fields,
objectMetadataMapItem.fields,
objectMetadataMap,
);
const selectedFields = graphqlFields(info);
const { select, relations } = graphqlQueryParser.parseSelectedFields(
objectMetadataItem,
const { relations } = graphqlQueryParser.parseSelectedFields(
objectMetadataMapItem,
selectedFields,
);
@@ -56,24 +58,59 @@ export class GraphqlQueryCreateManyResolverService {
skipUpdateIfNoValuesChanged: true,
});
const upsertedRecords = await repository.find({
where: {
const queryBuilder = repository.createQueryBuilder(
objectMetadataMapItem.nameSingular,
);
const nonFormattedUpsertedRecords = (await queryBuilder
.where({
id: In(objectRecords.generatedMaps.map((record) => record.id)),
},
select,
relations,
});
})
.take(QUERY_MAX_RECORDS)
.getMany()) as ObjectRecord[];
const upsertedRecords = formatResult(
nonFormattedUpsertedRecords,
objectMetadataMapItem,
objectMetadataMap,
);
const processNestedRelationsHelper = new ProcessNestedRelationsHelper();
if (relations) {
await processNestedRelationsHelper.processNestedRelations(
objectMetadataMap,
objectMetadataMapItem,
upsertedRecords,
relations,
QUERY_MAX_RECORDS,
authContext,
dataSource,
);
}
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionMapper(objectMetadataMap);
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
return upsertedRecords.map((record: ObjectRecord) =>
typeORMObjectRecordsParser.processRecord(
record,
objectMetadataItem.nameSingular,
objectMetadataMapItem.nameSingular,
1,
1,
),
);
}
async validate<ObjectRecord extends IRecord>(
args: CreateManyResolverArgs<Partial<ObjectRecord>>,
options: WorkspaceQueryRunnerOptions,
): Promise<void> {
assertMutationNotOnRemoteObject(options.objectMetadataItem);
args.data.forEach((record) => {
if (record?.id) {
assertIsValidUuid(record.id);
}
});
}
}

View File

@@ -1,33 +1,68 @@
import { Injectable } from '@nestjs/common';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { DestroyOneResolverArgs } 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 { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
export class GraphqlQueryDestroyOneResolverService {
private twentyORMGlobalManager: TwentyORMGlobalManager;
@Injectable()
export class GraphqlQueryDestroyOneResolverService
implements ResolverService<DestroyOneResolverArgs, IRecord>
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
constructor(twentyORMGlobalManager: TwentyORMGlobalManager) {
this.twentyORMGlobalManager = twentyORMGlobalManager;
}
async destroyOne<ObjectRecord extends IRecord = IRecord>(
async resolve<ObjectRecord extends IRecord = IRecord>(
args: DestroyOneResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord> {
const { authContext, objectMetadataItem } = options;
const { authContext, objectMetadataMapItem, objectMetadataMap } = options;
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
authContext.workspace.id,
objectMetadataItem.nameSingular,
objectMetadataMapItem.nameSingular,
);
const record = await repository.findOne({
const nonFormattedRecordBeforeDeletion = await repository.findOne({
where: { id: args.id },
withDeleted: true,
});
if (!nonFormattedRecordBeforeDeletion) {
throw new GraphqlQueryRunnerException(
'Record not found',
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
);
}
const recordBeforeDeletion = formatResult(
[nonFormattedRecordBeforeDeletion],
objectMetadataMapItem,
objectMetadataMap,
)[0];
await repository.delete(args.id);
return record as ObjectRecord;
return recordBeforeDeletion as ObjectRecord;
}
async validate(
args: DestroyOneResolverArgs,
_options: WorkspaceQueryRunnerOptions,
): Promise<void> {
if (!args.id) {
throw new GraphqlQueryRunnerException(
'Missing id',
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
}
}

View File

@@ -0,0 +1,214 @@
import { Injectable } from '@nestjs/common';
import isEmpty from 'lodash.isempty';
import { In } from 'typeorm';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
import {
Record as IRecord,
OrderByDirection,
RecordFilter,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { FindDuplicatesResolverArgs } 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 { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
import { settings } from 'src/engine/constants/settings';
import { DUPLICATE_CRITERIA_COLLECTION } from 'src/engine/core-modules/duplicate/constants/duplicate-criteria.constants';
import { ObjectMetadataMapItem } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable()
export class GraphqlQueryFindDuplicatesResolverService
implements
ResolverService<FindDuplicatesResolverArgs, IConnection<IRecord>[]>
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
async resolve<ObjectRecord extends IRecord = IRecord>(
args: FindDuplicatesResolverArgs<Partial<ObjectRecord>>,
options: WorkspaceQueryRunnerOptions,
): Promise<IConnection<ObjectRecord>[]> {
const { authContext, objectMetadataMapItem, objectMetadataMap } = options;
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
authContext.workspace.id,
);
const repository = dataSource.getRepository(
objectMetadataMapItem.nameSingular,
);
const existingRecordsQueryBuilder = repository.createQueryBuilder(
objectMetadataMapItem.nameSingular,
);
const duplicateRecordsQueryBuilder = repository.createQueryBuilder(
objectMetadataMapItem.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadataMap[objectMetadataMapItem.nameSingular].fields,
objectMetadataMap,
);
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
let objectRecords: Partial<ObjectRecord>[] = [];
if (args.ids) {
const nonFormattedObjectRecords = (await existingRecordsQueryBuilder
.where({ id: In(args.ids) })
.getMany()) as ObjectRecord[];
objectRecords = formatResult(
nonFormattedObjectRecords,
objectMetadataMapItem,
objectMetadataMap,
);
} else if (args.data && !isEmpty(args.data)) {
objectRecords = formatData(args.data, objectMetadataMapItem);
}
const duplicateConnections: IConnection<ObjectRecord>[] = await Promise.all(
objectRecords.map(async (record) => {
const duplicateConditions = this.buildDuplicateConditions(
objectMetadataMapItem,
[record],
record.id,
);
if (isEmpty(duplicateConditions)) {
return typeORMObjectRecordsParser.createConnection(
[],
objectMetadataMapItem.nameSingular,
0,
0,
[{ id: OrderByDirection.AscNullsFirst }],
false,
false,
);
}
const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
duplicateRecordsQueryBuilder,
objectMetadataMapItem.nameSingular,
duplicateConditions,
);
const nonFormattedDuplicates =
(await withFilterQueryBuilder.getMany()) as ObjectRecord[];
const duplicates = formatResult(
nonFormattedDuplicates,
objectMetadataMapItem,
objectMetadataMap,
);
return typeORMObjectRecordsParser.createConnection(
duplicates,
objectMetadataMapItem.nameSingular,
duplicates.length,
duplicates.length,
[{ id: OrderByDirection.AscNullsFirst }],
false,
false,
);
}),
);
return duplicateConnections;
}
private buildDuplicateConditions(
objectMetadataMapItem: ObjectMetadataMapItem,
records?: Partial<IRecord>[] | undefined,
filteringByExistingRecordId?: string,
): Partial<RecordFilter> {
if (!records || records.length === 0) {
return {};
}
const criteriaCollection = this.getApplicableDuplicateCriteriaCollection(
objectMetadataMapItem,
);
const conditions = records.flatMap((record) => {
const criteriaWithMatchingArgs = criteriaCollection.filter((criteria) =>
criteria.columnNames.every((columnName) => {
const value = record[columnName] as string | undefined;
return (
value && value.length >= settings.minLengthOfStringForDuplicateCheck
);
}),
);
return criteriaWithMatchingArgs.map((criteria) => {
const condition = {};
criteria.columnNames.forEach((columnName) => {
condition[columnName] = { eq: record[columnName] };
});
return condition;
});
});
const filter: Partial<RecordFilter> = {};
if (conditions && !isEmpty(conditions)) {
filter.or = conditions;
if (filteringByExistingRecordId) {
filter.id = { neq: filteringByExistingRecordId };
}
}
return filter;
}
private getApplicableDuplicateCriteriaCollection(
objectMetadataMapItem: ObjectMetadataMapItem,
) {
return DUPLICATE_CRITERIA_COLLECTION.filter(
(duplicateCriteria) =>
duplicateCriteria.objectName === objectMetadataMapItem.nameSingular,
);
}
async validate(
args: FindDuplicatesResolverArgs,
_options: WorkspaceQueryRunnerOptions,
): Promise<void> {
if (!args.data && !args.ids) {
throw new GraphqlQueryRunnerException(
'You have to provide either "data" or "ids" argument',
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
if (args.data && args.ids) {
throw new GraphqlQueryRunnerException(
'You cannot provide both "data" and "ids" arguments',
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
if (!args.ids && isEmpty(args.data)) {
throw new GraphqlQueryRunnerException(
'The "data" condition can not be empty when "ids" input not provided',
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
}
}

View File

@@ -1,6 +1,9 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'class-validator';
import graphqlFields from 'graphql-fields';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
import {
Record as IRecord,
OrderByDirection,
@@ -17,26 +20,25 @@ import {
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
import { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper';
import { computeCursorArgFilter } from 'src/engine/api/graphql/graphql-query-runner/utils/compute-cursor-arg-filter';
import { decodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
import { getObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-object-metadata-or-throw.util';
import {
ObjectMetadataMapItem,
generateObjectMetadataMap,
} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
getCursor,
getPaginationInfo,
} from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
export class GraphqlQueryFindManyResolverService {
private twentyORMGlobalManager: TwentyORMGlobalManager;
@Injectable()
export class GraphqlQueryFindManyResolverService
implements ResolverService<FindManyResolverArgs, IConnection<IRecord>>
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
constructor(twentyORMGlobalManager: TwentyORMGlobalManager) {
this.twentyORMGlobalManager = twentyORMGlobalManager;
}
async findMany<
async resolve<
ObjectRecord extends IRecord = IRecord,
Filter extends RecordFilter = RecordFilter,
OrderBy extends RecordOrderBy = RecordOrderBy,
@@ -44,51 +46,41 @@ export class GraphqlQueryFindManyResolverService {
args: FindManyResolverArgs<Filter, OrderBy>,
options: WorkspaceQueryRunnerOptions,
): Promise<IConnection<ObjectRecord>> {
const { authContext, objectMetadataItem, info, objectMetadataCollection } =
const { authContext, objectMetadataMapItem, info, objectMetadataMap } =
options;
this.validateArgsOrThrow(args);
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
authContext.workspace.id,
);
const repository = dataSource.getRepository(
objectMetadataItem.nameSingular,
objectMetadataMapItem.nameSingular,
);
const queryBuilder = repository.createQueryBuilder(
objectMetadataItem.nameSingular,
objectMetadataMapItem.nameSingular,
);
const countQueryBuilder = repository.createQueryBuilder(
objectMetadataItem.nameSingular,
objectMetadataMapItem.nameSingular,
);
const objectMetadataMap = generateObjectMetadataMap(
objectMetadataCollection,
);
const objectMetadata = getObjectMetadataOrThrow(
objectMetadataMap,
objectMetadataItem.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadata.fields,
objectMetadataMapItem.fields,
objectMetadataMap,
);
const withFilterCountQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
countQueryBuilder,
objectMetadataItem.nameSingular,
objectMetadataMapItem.nameSingular,
args.filter ?? ({} as Filter),
);
const selectedFields = graphqlFields(info);
const { relations } = graphqlQueryParser.parseSelectedFields(
objectMetadataItem,
objectMetadataMapItem,
selectedFields,
);
const isForwardPagination = !isDefined(args.before);
@@ -105,7 +97,7 @@ export class GraphqlQueryFindManyResolverService {
? await withDeletedCountQueryBuilder.getCount()
: 0;
const cursor = this.getCursor(args);
const cursor = getCursor(args);
let appliedFilters = args.filter ?? ({} as Filter);
@@ -118,7 +110,7 @@ export class GraphqlQueryFindManyResolverService {
const cursorArgFilter = computeCursorArgFilter(
cursor,
orderByWithIdCondition,
objectMetadata.fields,
objectMetadataMapItem.fields,
isForwardPagination,
);
@@ -131,14 +123,14 @@ export class GraphqlQueryFindManyResolverService {
const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
queryBuilder,
objectMetadataItem.nameSingular,
objectMetadataMapItem.nameSingular,
appliedFilters,
);
const withOrderByQueryBuilder = graphqlQueryParser.applyOrderToBuilder(
withFilterQueryBuilder,
orderByWithIdCondition,
objectMetadataItem.nameSingular,
objectMetadataMapItem.nameSingular,
isForwardPagination,
);
@@ -153,11 +145,11 @@ export class GraphqlQueryFindManyResolverService {
const objectRecords = formatResult(
nonFormattedObjectRecords,
objectMetadata,
objectMetadataMapItem,
objectMetadataMap,
);
const { hasNextPage, hasPreviousPage } = this.getPaginationInfo(
const { hasNextPage, hasPreviousPage } = getPaginationInfo(
objectRecords,
limit,
isForwardPagination,
@@ -167,14 +159,12 @@ export class GraphqlQueryFindManyResolverService {
objectRecords.pop();
}
const processNestedRelationsHelper = new ProcessNestedRelationsHelper(
this.twentyORMGlobalManager,
);
const processNestedRelationsHelper = new ProcessNestedRelationsHelper();
if (relations) {
await processNestedRelationsHelper.processNestedRelations(
objectMetadataMap,
objectMetadata,
objectMetadataMapItem,
objectRecords,
relations,
limit,
@@ -184,20 +174,25 @@ export class GraphqlQueryFindManyResolverService {
}
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionMapper(objectMetadataMap);
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
return typeORMObjectRecordsParser.createConnection(
const result = typeORMObjectRecordsParser.createConnection(
objectRecords,
objectMetadataItem.nameSingular,
objectMetadataMapItem.nameSingular,
limit,
totalCount,
orderByWithIdCondition,
hasNextPage,
hasPreviousPage,
);
return result;
}
private validateArgsOrThrow(args: FindManyResolverArgs<any, any>) {
async validate<Filter extends RecordFilter>(
args: FindManyResolverArgs<Filter>,
_options: WorkspaceQueryRunnerOptions,
): Promise<void> {
if (args.first && args.last) {
throw new GraphqlQueryRunnerException(
'Cannot provide both first and last',
@@ -235,49 +230,4 @@ export class GraphqlQueryFindManyResolverService {
);
}
}
private getCursor(
args: FindManyResolverArgs<any, any>,
): Record<string, any> | undefined {
if (args.after) return decodeCursor(args.after);
if (args.before) return decodeCursor(args.before);
return undefined;
}
private addOrderByColumnsToSelect(
order: Record<string, any>,
select: Record<string, boolean>,
) {
for (const column of Object.keys(order || {})) {
if (!select[column]) {
select[column] = true;
}
}
}
private addForeingKeyColumnsToSelect(
relations: Record<string, any>,
select: Record<string, boolean>,
objectMetadata: ObjectMetadataMapItem,
) {
for (const column of Object.keys(relations || {})) {
if (!select[`${column}Id`] && objectMetadata.fields[`${column}Id`]) {
select[`${column}Id`] = true;
}
}
}
private getPaginationInfo(
objectRecords: any[],
limit: number,
isForwardPagination: boolean,
) {
const hasMoreRecords = objectRecords.length > limit;
return {
hasNextPage: isForwardPagination && hasMoreRecords,
hasPreviousPage: !isForwardPagination && hasMoreRecords,
};
}
}

View File

@@ -1,5 +1,8 @@
import { Injectable } from '@nestjs/common';
import graphqlFields from 'graphql-fields';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
import {
Record as IRecord,
RecordFilter,
@@ -13,28 +16,31 @@ import {
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
import { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper';
import { getObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-object-metadata-or-throw.util';
import { generateObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import {
WorkspaceQueryRunnerException,
WorkspaceQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
export class GraphqlQueryFindOneResolverService {
private twentyORMGlobalManager: TwentyORMGlobalManager;
@Injectable()
export class GraphqlQueryFindOneResolverService
implements ResolverService<FindOneResolverArgs, IRecord>
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
constructor(twentyORMGlobalManager: TwentyORMGlobalManager) {
this.twentyORMGlobalManager = twentyORMGlobalManager;
}
async findOne<
async resolve<
ObjectRecord extends IRecord = IRecord,
Filter extends RecordFilter = RecordFilter,
>(
args: FindOneResolverArgs<Filter>,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord | undefined> {
const { authContext, objectMetadataItem, info, objectMetadataCollection } =
): Promise<ObjectRecord> {
const { authContext, objectMetadataMapItem, info, objectMetadataMap } =
options;
const dataSource =
@@ -43,37 +49,28 @@ export class GraphqlQueryFindOneResolverService {
);
const repository = dataSource.getRepository(
objectMetadataItem.nameSingular,
objectMetadataMapItem.nameSingular,
);
const queryBuilder = repository.createQueryBuilder(
objectMetadataItem.nameSingular,
);
const objectMetadataMap = generateObjectMetadataMap(
objectMetadataCollection,
);
const objectMetadata = getObjectMetadataOrThrow(
objectMetadataMap,
objectMetadataItem.nameSingular,
objectMetadataMapItem.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadata.fields,
objectMetadataMapItem.fields,
objectMetadataMap,
);
const selectedFields = graphqlFields(info);
const { relations } = graphqlQueryParser.parseSelectedFields(
objectMetadataItem,
objectMetadataMapItem,
selectedFields,
);
const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
queryBuilder,
objectMetadataItem.nameSingular,
objectMetadataMapItem.nameSingular,
args.filter ?? ({} as Filter),
);
@@ -86,12 +83,10 @@ export class GraphqlQueryFindOneResolverService {
const objectRecord = formatResult(
nonFormattedObjectRecord,
objectMetadata,
objectMetadataMapItem,
objectMetadataMap,
);
const limit = QUERY_MAX_RECORDS;
if (!objectRecord) {
throw new GraphqlQueryRunnerException(
'Record not found',
@@ -99,32 +94,42 @@ export class GraphqlQueryFindOneResolverService {
);
}
const processNestedRelationsHelper = new ProcessNestedRelationsHelper(
this.twentyORMGlobalManager,
);
const processNestedRelationsHelper = new ProcessNestedRelationsHelper();
const objectRecords = [objectRecord];
if (relations) {
await processNestedRelationsHelper.processNestedRelations(
objectMetadataMap,
objectMetadata,
objectMetadataMapItem,
objectRecords,
relations,
limit,
QUERY_MAX_RECORDS,
authContext,
dataSource,
);
}
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionMapper(objectMetadataMap);
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
return typeORMObjectRecordsParser.processRecord(
objectRecords[0],
objectMetadataItem.nameSingular,
objectMetadataMapItem.nameSingular,
1,
1,
) as ObjectRecord;
}
async validate<Filter extends RecordFilter>(
args: FindOneResolverArgs<Filter>,
_options: WorkspaceQueryRunnerOptions,
): Promise<void> {
if (!args.filter || Object.keys(args.filter).length === 0) {
throw new WorkspaceQueryRunnerException(
'Missing filter argument',
WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
}
}

View File

@@ -1,3 +1,6 @@
import { Injectable } from '@nestjs/common';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
import {
Record as IRecord,
OrderByDirection,
@@ -11,47 +14,25 @@ import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper';
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
import { generateObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
export class GraphqlQuerySearchResolverService {
private twentyORMGlobalManager: TwentyORMGlobalManager;
private featureFlagService: FeatureFlagService;
@Injectable()
export class GraphqlQuerySearchResolverService
implements ResolverService<SearchResolverArgs, IConnection<IRecord>>
{
constructor(
twentyORMGlobalManager: TwentyORMGlobalManager,
featureFlagService: FeatureFlagService,
) {
this.twentyORMGlobalManager = twentyORMGlobalManager;
this.featureFlagService = featureFlagService;
}
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly featureFlagService: FeatureFlagService,
) {}
async search<ObjectRecord extends IRecord = IRecord>(
async resolve<ObjectRecord extends IRecord = IRecord>(
args: SearchResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<IConnection<ObjectRecord>> {
const { authContext, objectMetadataItem, objectMetadataCollection } =
options;
const featureFlagsForWorkspace =
await this.featureFlagService.getWorkspaceFeatureFlags(
authContext.workspace.id,
);
const isQueryRunnerTwentyORMEnabled =
featureFlagsForWorkspace.IS_QUERY_RUNNER_TWENTY_ORM_ENABLED;
const isSearchEnabled = featureFlagsForWorkspace.IS_SEARCH_ENABLED;
if (!isQueryRunnerTwentyORMEnabled || !isSearchEnabled) {
throw new GraphqlQueryRunnerException(
'This endpoint is not available yet, please use findMany instead.',
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
const { authContext, objectMetadataItem, objectMetadataMap } = options;
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
@@ -59,21 +40,8 @@ export class GraphqlQuerySearchResolverService {
objectMetadataItem.nameSingular,
);
const objectMetadataMap = generateObjectMetadataMap(
objectMetadataCollection,
);
const objectMetadata = objectMetadataMap[objectMetadataItem.nameSingular];
if (!objectMetadata) {
throw new GraphqlQueryRunnerException(
`Object metadata not found for ${objectMetadataItem.nameSingular}`,
GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
}
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionMapper(objectMetadataMap);
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
if (!args.searchInput) {
return typeORMObjectRecordsParser.createConnection(
@@ -100,7 +68,7 @@ export class GraphqlQuerySearchResolverService {
'DESC',
)
.setParameter('searchTerms', searchTerms)
.limit(limit)
.take(limit)
.getMany()) as ObjectRecord[];
const objectRecords = await repository.formatResult(resultsWithTsVector);
@@ -129,4 +97,26 @@ export class GraphqlQuerySearchResolverService {
return formattedWords.join(' | ');
}
async validate(
_args: SearchResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<void> {
const featureFlagsForWorkspace =
await this.featureFlagService.getWorkspaceFeatureFlags(
options.authContext.workspace.id,
);
const isQueryRunnerTwentyORMEnabled =
featureFlagsForWorkspace.IS_QUERY_RUNNER_TWENTY_ORM_ENABLED;
const isSearchEnabled = featureFlagsForWorkspace.IS_SEARCH_ENABLED;
if (!isQueryRunnerTwentyORMEnabled || !isSearchEnabled) {
throw new GraphqlQueryRunnerException(
'This endpoint is not available yet, please use findMany instead.',
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
}
}

View File

@@ -0,0 +1,116 @@
import { Injectable } from '@nestjs/common';
import graphqlFields from 'graphql-fields';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { UpdateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable()
export class GraphqlQueryUpdateManyResolverService
implements ResolverService<UpdateManyResolverArgs, IRecord[]>
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
async resolve<ObjectRecord extends IRecord = IRecord>(
args: UpdateManyResolverArgs<Partial<ObjectRecord>>,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord[]> {
const { authContext, objectMetadataMapItem, objectMetadataMap, info } =
options;
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
authContext.workspace.id,
);
const repository = dataSource.getRepository(
objectMetadataMapItem.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadataMapItem.fields,
objectMetadataMap,
);
const selectedFields = graphqlFields(info);
const { relations } = graphqlQueryParser.parseSelectedFields(
objectMetadataMapItem,
selectedFields,
);
const queryBuilder = repository.createQueryBuilder(
objectMetadataMapItem.nameSingular,
);
const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
queryBuilder,
objectMetadataMapItem.nameSingular,
args.filter,
);
const data = formatData(args.data, objectMetadataMapItem);
const result = await withFilterQueryBuilder
.update()
.set(data)
.returning('*')
.execute();
const nonFormattedUpdatedObjectRecords = result.raw;
const updatedRecords = formatResult(
nonFormattedUpdatedObjectRecords,
objectMetadataMapItem,
objectMetadataMap,
);
const processNestedRelationsHelper = new ProcessNestedRelationsHelper();
if (relations) {
await processNestedRelationsHelper.processNestedRelations(
objectMetadataMap,
objectMetadataMapItem,
updatedRecords,
relations,
QUERY_MAX_RECORDS,
authContext,
dataSource,
);
}
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
return updatedRecords.map((record: ObjectRecord) =>
typeORMObjectRecordsParser.processRecord(
record,
objectMetadataMapItem.nameSingular,
1,
1,
),
);
}
async validate<ObjectRecord extends IRecord = IRecord>(
args: UpdateManyResolverArgs<Partial<ObjectRecord>>,
options: WorkspaceQueryRunnerOptions,
): Promise<void> {
assertMutationNotOnRemoteObject(options.objectMetadataMapItem);
args.filter?.id?.in?.forEach((id: string) => assertIsValidUuid(id));
}
}

View File

@@ -0,0 +1,123 @@
import { Injectable } from '@nestjs/common';
import graphqlFields from 'graphql-fields';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { UpdateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant';
import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable()
export class GraphqlQueryUpdateOneResolverService
implements ResolverService<UpdateOneResolverArgs, IRecord>
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
async resolve<ObjectRecord extends IRecord = IRecord>(
args: UpdateOneResolverArgs<Partial<ObjectRecord>>,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord> {
const { authContext, objectMetadataMapItem, objectMetadataMap, info } =
options;
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
authContext.workspace.id,
);
const repository = dataSource.getRepository(
objectMetadataMapItem.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadataMapItem.fields,
objectMetadataMap,
);
const selectedFields = graphqlFields(info);
const { relations } = graphqlQueryParser.parseSelectedFields(
objectMetadataMapItem,
selectedFields,
);
const queryBuilder = repository.createQueryBuilder(
objectMetadataMapItem.nameSingular,
);
const withFilterQueryBuilder = queryBuilder.where({ id: args.id });
const data = formatData(args.data, objectMetadataMapItem);
const result = await withFilterQueryBuilder
.update()
.set(data)
.returning('*')
.execute();
const nonFormattedUpdatedObjectRecords = result.raw;
const updatedRecords = formatResult(
nonFormattedUpdatedObjectRecords,
objectMetadataMapItem,
objectMetadataMap,
);
if (updatedRecords.length === 0) {
throw new GraphqlQueryRunnerException(
'Record not found',
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
);
}
const updatedRecord = updatedRecords[0] as ObjectRecord;
const processNestedRelationsHelper = new ProcessNestedRelationsHelper();
if (relations) {
await processNestedRelationsHelper.processNestedRelations(
objectMetadataMap,
objectMetadataMapItem,
[updatedRecord],
relations,
QUERY_MAX_RECORDS,
authContext,
dataSource,
);
}
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
return typeORMObjectRecordsParser.processRecord<ObjectRecord>(
updatedRecord,
objectMetadataMapItem.nameSingular,
1,
1,
);
}
async validate<ObjectRecord extends IRecord = IRecord>(
args: UpdateOneResolverArgs<Partial<ObjectRecord>>,
options: WorkspaceQueryRunnerOptions,
): Promise<void> {
assertMutationNotOnRemoteObject(options.objectMetadataMapItem);
assertIsValidUuid(args.id);
}
}

View File

@@ -0,0 +1,137 @@
import { Injectable } from '@nestjs/common';
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
@Injectable()
export class ApiEventEmitterService {
constructor(private readonly workspaceEventEmitter: WorkspaceEventEmitter) {}
public emitCreateEvents<T extends IRecord>(
records: T[],
authContext: AuthContext,
objectMetadataItem: ObjectMetadataInterface,
): void {
this.workspaceEventEmitter.emit(
`${objectMetadataItem.nameSingular}.created`,
records.map((record) => ({
userId: authContext.user?.id,
recordId: record.id,
objectMetadata: objectMetadataItem,
properties: {
before: null,
after: this.removeGraphQLAndNestedProperties(record),
},
})),
authContext.workspace.id,
);
}
public emitUpdateEvents<T extends IRecord>(
existingRecords: T[],
records: T[],
updatedFields: string[],
authContext: AuthContext,
objectMetadataItem: ObjectMetadataInterface,
): void {
const mappedExistingRecords = existingRecords.reduce(
(acc, { id, ...record }) => ({
...acc,
[id]: record,
}),
{},
);
this.workspaceEventEmitter.emit(
`${objectMetadataItem.nameSingular}.updated`,
records.map((record) => {
return {
userId: authContext.user?.id,
recordId: record.id,
objectMetadata: objectMetadataItem,
properties: {
before: mappedExistingRecords[record.id]
? this.removeGraphQLAndNestedProperties(
mappedExistingRecords[record.id],
)
: undefined,
after: this.removeGraphQLAndNestedProperties(record),
updatedFields,
},
};
}),
authContext.workspace.id,
);
}
public emitDeletedEvents<T extends IRecord>(
records: T[],
authContext: AuthContext,
objectMetadataItem: ObjectMetadataInterface,
): void {
this.workspaceEventEmitter.emit(
`${objectMetadataItem.nameSingular}.deleted`,
records.map((record) => {
return {
userId: authContext.user?.id,
recordId: record.id,
objectMetadata: objectMetadataItem,
properties: {
before: this.removeGraphQLAndNestedProperties(record),
after: null,
},
};
}),
authContext.workspace.id,
);
}
public emitDestroyEvents<T extends IRecord>(
records: T[],
authContext: AuthContext,
objectMetadataItem: ObjectMetadataInterface,
): void {
this.workspaceEventEmitter.emit(
`${objectMetadataItem.nameSingular}.destroyed`,
records.map((record) => {
return {
userId: authContext.user?.id,
recordId: record.id,
objectMetadata: objectMetadataItem,
properties: {
before: this.removeGraphQLAndNestedProperties(record),
after: null,
},
};
}),
authContext.workspace.id,
);
}
private removeGraphQLAndNestedProperties<ObjectRecord extends IRecord>(
record: ObjectRecord,
) {
if (!record) {
return {};
}
const sanitizedRecord = {};
for (const [key, value] of Object.entries(record)) {
if (value && typeof value === 'object' && value['edges']) {
continue;
}
if (key === '__typename') {
continue;
}
sanitizedRecord[key] = value;
}
return sanitizedRecord;
}
}

View File

@@ -2,6 +2,7 @@ import {
Record as IRecord,
RecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import {
GraphqlQueryRunnerException,
@@ -44,3 +45,25 @@ export const encodeCursor = <ObjectRecord extends IRecord = IRecord>(
return Buffer.from(JSON.stringify(cursorData)).toString('base64');
};
export const getCursor = (
args: FindManyResolverArgs<any, any>,
): Record<string, any> | undefined => {
if (args.after) return decodeCursor(args.after);
if (args.before) return decodeCursor(args.before);
return undefined;
};
export const getPaginationInfo = (
objectRecords: any[],
limit: number,
isForwardPagination: boolean,
) => {
const hasMoreRecords = objectRecords.length > limit;
return {
hasNextPage: isForwardPagination && hasMoreRecords,
hasPreviousPage: !isForwardPagination && hasMoreRecords,
};
};

View File

@@ -4,6 +4,10 @@ import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metada
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import {
ObjectMetadataMap,
ObjectMetadataMapItem,
} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
export interface WorkspaceQueryRunnerOptions {
authContext: AuthContext;
@@ -11,4 +15,6 @@ export interface WorkspaceQueryRunnerOptions {
objectMetadataItem: ObjectMetadataInterface;
fieldMetadataCollection: FieldMetadataInterface[];
objectMetadataCollection: ObjectMetadataInterface[];
objectMetadataMap: ObjectMetadataMap;
objectMetadataMapItem: ObjectMetadataMapItem;
}

View File

@@ -9,6 +9,7 @@ import {
FindManyResolverArgs,
FindOneResolverArgs,
RestoreManyResolverArgs,
SearchResolverArgs,
UpdateManyResolverArgs,
UpdateOneResolverArgs,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
@@ -42,4 +43,6 @@ export type WorkspacePreQueryHookPayload<T> = T extends 'createMany'
? DestroyManyResolverArgs
: T extends 'destroyOne'
? DestroyOneResolverArgs
: never;
: T extends 'search'
? SearchResolverArgs
: never;

View File

@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import {
CreateManyResolverArgs,
@@ -32,12 +33,14 @@ export class CreateManyResolverFactory
return async (_source, args, _context, info) => {
try {
const options = {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
objectMetadataMap: internalContext.objectMetadataMap,
objectMetadataMapItem: internalContext.objectMetadataMapItem,
};
const isQueryRunnerTwentyORMEnabled =

View File

@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import {
CreateOneResolverArgs,
@@ -32,12 +33,14 @@ export class CreateOneResolverFactory
return async (_source, args, _context, info) => {
try {
const options = {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
objectMetadataMap: internalContext.objectMetadataMap,
objectMetadataMapItem: internalContext.objectMetadataMapItem,
};
const isQueryRunnerTwentyORMEnabled =
@@ -50,13 +53,7 @@ export class CreateOneResolverFactory
return await this.graphqlQueryRunnerService.createOne(args, options);
}
return await this.workspaceQueryRunnerService.createOne(args, {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
});
return await this.workspaceQueryRunnerService.createOne(args, options);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}

View File

@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import {
DeleteManyResolverArgs,
@@ -7,8 +8,11 @@ import {
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.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';
@Injectable()
export class DeleteManyResolverFactory
@@ -18,6 +22,8 @@ export class DeleteManyResolverFactory
constructor(
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
private readonly featureFlagService: FeatureFlagService,
private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService,
) {}
create(
@@ -27,13 +33,27 @@ export class DeleteManyResolverFactory
return async (_source, args, context, info) => {
try {
return await this.workspaceQueryRunnerService.deleteMany(args, {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
});
objectMetadataMap: internalContext.objectMetadataMap,
objectMetadataMapItem: internalContext.objectMetadataMapItem,
};
const isQueryRunnerTwentyORMEnabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsQueryRunnerTwentyORMEnabled,
internalContext.authContext.workspace.id,
);
if (isQueryRunnerTwentyORMEnabled) {
return await this.graphqlQueryRunnerService.deleteMany(args, options);
}
return await this.workspaceQueryRunnerService.deleteMany(args, options);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}

View File

@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import {
DeleteOneResolverArgs,
@@ -7,8 +8,11 @@ import {
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.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';
@Injectable()
export class DeleteOneResolverFactory
@@ -18,6 +22,8 @@ export class DeleteOneResolverFactory
constructor(
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
private readonly featureFlagService: FeatureFlagService,
private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService,
) {}
create(
@@ -27,13 +33,27 @@ export class DeleteOneResolverFactory
return async (_source, args, context, info) => {
try {
return await this.workspaceQueryRunnerService.deleteOne(args, {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
});
objectMetadataMap: internalContext.objectMetadataMap,
objectMetadataMapItem: internalContext.objectMetadataMapItem,
};
const isQueryRunnerTwentyORMEnabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsQueryRunnerTwentyORMEnabled,
internalContext.authContext.workspace.id,
);
if (isQueryRunnerTwentyORMEnabled) {
return await this.graphqlQueryRunnerService.deleteOne(args, options);
}
return await this.workspaceQueryRunnerService.deleteOne(args, options);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}

View File

@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import {
DestroyManyResolverArgs,
@@ -27,13 +28,20 @@ export class DestroyManyResolverFactory
return async (_source, args, context, info) => {
try {
return await this.workspaceQueryRunnerService.destroyMany(args, {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
});
objectMetadataMap: internalContext.objectMetadataMap,
objectMetadataMapItem: internalContext.objectMetadataMapItem,
};
return await this.workspaceQueryRunnerService.destroyMany(
args,
options,
);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}

View File

@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import {
DestroyOneResolverArgs,
@@ -27,13 +28,17 @@ export class DestroyOneResolverFactory
return async (_source, args, context, info) => {
try {
return await this.graphQLQueryRunnerService.destroyOne(args, {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
});
objectMetadataMap: internalContext.objectMetadataMap,
objectMetadataMapItem: internalContext.objectMetadataMapItem,
};
return await this.graphQLQueryRunnerService.destroyOne(args, options);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}

View File

@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import {
FindDuplicatesResolverArgs,
@@ -7,8 +8,11 @@ import {
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.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';
@Injectable()
export class FindDuplicatesResolverFactory
@@ -18,6 +22,8 @@ export class FindDuplicatesResolverFactory
constructor(
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
private readonly featureFlagService: FeatureFlagService,
private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService,
) {}
create(
@@ -27,13 +33,33 @@ export class FindDuplicatesResolverFactory
return async (_source, args, context, info) => {
try {
return await this.workspaceQueryRunnerService.findDuplicates(args, {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
});
objectMetadataMap: internalContext.objectMetadataMap,
objectMetadataMapItem: internalContext.objectMetadataMapItem,
};
const isQueryRunnerTwentyORMEnabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsQueryRunnerTwentyORMEnabled,
internalContext.authContext.workspace.id,
);
if (isQueryRunnerTwentyORMEnabled) {
return await this.graphqlQueryRunnerService.findDuplicates(
args,
options,
);
}
return await this.workspaceQueryRunnerService.findDuplicates(
args,
options,
);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}

View File

@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import {
FindManyResolverArgs,
@@ -27,12 +28,14 @@ export class FindManyResolverFactory
return async (_source, args, _context, info) => {
try {
const options = {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
objectMetadataMap: internalContext.objectMetadataMap,
objectMetadataMapItem: internalContext.objectMetadataMapItem,
};
return await this.graphqlQueryRunnerService.findMany(args, options);

View File

@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import {
FindOneResolverArgs,
@@ -27,12 +28,14 @@ export class FindOneResolverFactory
return async (_source, args, _context, info) => {
try {
const options = {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
objectMetadataMap: internalContext.objectMetadataMap,
objectMetadataMapItem: internalContext.objectMetadataMapItem,
};
return await this.graphqlQueryRunnerService.findOne(args, options);

View File

@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import {
Resolver,
@@ -7,8 +8,11 @@ import {
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.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';
@Injectable()
export class RestoreManyResolverFactory
@@ -18,6 +22,8 @@ export class RestoreManyResolverFactory
constructor(
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
private readonly featureFlagService: FeatureFlagService,
private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService,
) {}
create(
@@ -27,13 +33,33 @@ export class RestoreManyResolverFactory
return async (_source, args, context, info) => {
try {
return await this.workspaceQueryRunnerService.restoreMany(args, {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
});
objectMetadataMap: internalContext.objectMetadataMap,
objectMetadataMapItem: internalContext.objectMetadataMapItem,
};
const isQueryRunnerTwentyORMEnabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsQueryRunnerTwentyORMEnabled,
internalContext.authContext.workspace.id,
);
if (isQueryRunnerTwentyORMEnabled) {
return await this.graphqlQueryRunnerService.restoreMany(
args,
options,
);
}
return await this.workspaceQueryRunnerService.restoreMany(
args,
options,
);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}

View File

@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import {
Resolver,
@@ -25,13 +26,17 @@ export class SearchResolverFactory
return async (_source, args, _context, info) => {
try {
return await this.graphqlQueryRunnerService.search(args, {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
});
objectMetadataMap: internalContext.objectMetadataMap,
objectMetadataMapItem: internalContext.objectMetadataMapItem,
};
return await this.graphqlQueryRunnerService.search(args, options);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}

View File

@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import {
Resolver,
@@ -7,8 +8,11 @@ import {
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.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';
@Injectable()
export class UpdateManyResolverFactory
@@ -18,6 +22,8 @@ export class UpdateManyResolverFactory
constructor(
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
private readonly featureFlagService: FeatureFlagService,
private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService,
) {}
create(
@@ -27,13 +33,27 @@ export class UpdateManyResolverFactory
return async (_source, args, context, info) => {
try {
return await this.workspaceQueryRunnerService.updateMany(args, {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
});
objectMetadataMap: internalContext.objectMetadataMap,
objectMetadataMapItem: internalContext.objectMetadataMapItem,
};
const isQueryRunnerTwentyORMEnabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsQueryRunnerTwentyORMEnabled,
internalContext.authContext.workspace.id,
);
if (isQueryRunnerTwentyORMEnabled) {
return await this.graphqlQueryRunnerService.updateMany(args, options);
}
return await this.workspaceQueryRunnerService.updateMany(args, options);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}

View File

@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import {
Resolver,
@@ -7,8 +8,11 @@ import {
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.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';
@Injectable()
export class UpdateOneResolverFactory
@@ -18,6 +22,8 @@ export class UpdateOneResolverFactory
constructor(
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
private readonly featureFlagService: FeatureFlagService,
private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService,
) {}
create(
@@ -27,13 +33,27 @@ export class UpdateOneResolverFactory
return async (_source, args, context, info) => {
try {
return await this.workspaceQueryRunnerService.updateOne(args, {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
});
objectMetadataMap: internalContext.objectMetadataMap,
objectMetadataMapItem: internalContext.objectMetadataMapItem,
};
const isQueryRunnerTwentyORMEnabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsQueryRunnerTwentyORMEnabled,
internalContext.authContext.workspace.id,
);
if (isQueryRunnerTwentyORMEnabled) {
return await this.graphqlQueryRunnerService.updateOne(args, options);
}
return await this.workspaceQueryRunnerService.updateOne(args, options);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}

View File

@@ -11,6 +11,7 @@ import { RestoreManyResolverFactory } from 'src/engine/api/graphql/workspace-res
import { SearchResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/search-resolver-factory';
import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { getResolverName } from 'src/engine/utils/get-resolver-name.util';
import { CreateManyResolverFactory } from './factories/create-many-resolver.factory';
@@ -49,6 +50,7 @@ export class WorkspaceResolverFactory {
async create(
authContext: AuthContext,
objectMetadataCollection: ObjectMetadataInterface[],
objectMetadataMap: ObjectMetadataMap,
workspaceResolverBuilderMethods: WorkspaceResolverBuilderMethods,
): Promise<IResolvers> {
const factories = new Map<
@@ -94,7 +96,9 @@ export class WorkspaceResolverFactory {
authContext,
objectMetadataItem: objectMetadata,
fieldMetadataCollection: objectMetadata.fields,
objectMetadataCollection: objectMetadataCollection,
objectMetadataCollection,
objectMetadataMap,
objectMetadataMapItem: objectMetadataMap[objectMetadata.nameSingular],
});
}
@@ -117,7 +121,9 @@ export class WorkspaceResolverFactory {
authContext,
objectMetadataItem: objectMetadata,
fieldMetadataCollection: objectMetadata.fields,
objectMetadataCollection: objectMetadataCollection,
objectMetadataCollection,
objectMetadataMap,
objectMetadataMapItem: objectMetadataMap[objectMetadata.nameSingular],
});
}
}

View File

@@ -2,10 +2,16 @@ import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metada
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import {
ObjectMetadataMap,
ObjectMetadataMapItem,
} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
export interface WorkspaceSchemaBuilderContext {
authContext: AuthContext;
objectMetadataItem: ObjectMetadataInterface;
fieldMetadataCollection: FieldMetadataInterface[];
objectMetadataCollection: ObjectMetadataInterface[];
objectMetadataItem: ObjectMetadataInterface;
objectMetadataMap: ObjectMetadataMap;
objectMetadataMapItem: ObjectMetadataMapItem;
}

View File

@@ -117,6 +117,7 @@ export class WorkspaceSchemaFactory {
const autoGeneratedResolvers = await this.workspaceResolverFactory.create(
authContext,
objectMetadataCollection,
objectMetadataMap,
workspaceResolverBuilderMethodNames,
);
const scalarsResolvers =

View File

@@ -1,15 +1,15 @@
import { Injectable } from '@nestjs/common';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import {
Record as IRecord,
Record,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { settings } from 'src/engine/constants/settings';
import { DUPLICATE_CRITERIA_COLLECTION } from 'src/engine/core-modules/duplicate/constants/duplicate-criteria.constants';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { DUPLICATE_CRITERIA_COLLECTION } from 'src/engine/core-modules/duplicate/constants/duplicate-criteria.constants';
@Injectable()
export class DuplicateService {
@@ -94,80 +94,4 @@ export class DuplicateService {
duplicateCriteria.objectName === objectMetadataItem.nameSingular,
);
}
/**
* TODO: Remove this code by September 1st, 2024 if it isn't used
* It was build to be used by the upsertMany function, but it was not used.
* It's a re-implementation of the methods to findDuplicates, but done
* at the SQL layer instead of doing it at the GraphQL layer
*
async findDuplicate(
data: Partial<Record>,
objectMetadata: ObjectMetadataInterface,
workspaceId: string,
) {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const { duplicateWhereClause, duplicateWhereParameters } =
this.buildDuplicateConditionForUpsert(objectMetadata, data);
const results = await this.workspaceDataSourceService.executeRawQuery(
`
SELECT
*
FROM
${dataSourceSchema}."${computeObjectTargetTable(
objectMetadata,
)}" p
WHERE
${duplicateWhereClause}
`,
duplicateWhereParameters,
workspaceId,
);
return results.length > 0 ? results[0] : null;
}
private buildDuplicateConditionForUpsert(
objectMetadata: ObjectMetadataInterface,
data: Partial<Record>,
) {
const criteriaCollection = this.getApplicableDuplicateCriteriaCollection(
objectMetadata,
).filter(
(duplicateCriteria) => duplicateCriteria.useAsUniqueKeyForUpsert === true,
);
const whereClauses: string[] = [];
const whereParameters: any[] = [];
let parameterIndex = 1;
criteriaCollection.forEach((c) => {
const clauseParts: string[] = [];
c.columnNames.forEach((column) => {
const dataKey = Object.keys(data).find(
(key) => key.toLowerCase() === column.toLowerCase(),
);
if (dataKey) {
clauseParts.push(`p."${column}" = $${parameterIndex}`);
whereParameters.push(data[dataKey]);
parameterIndex++;
}
});
if (clauseParts.length > 0) {
whereClauses.push(`(${clauseParts.join(' AND ')})`);
}
});
const duplicateWhereClause = whereClauses.join(' OR ');
const duplicateWhereParameters = whereParameters;
return { duplicateWhereClause, duplicateWhereParameters };
}
*
*/
}

View File

@@ -60,8 +60,8 @@ export class IndexMetadataEntity {
@Column({
type: 'enum',
enum: IndexType,
nullable: true,
default: IndexType.BTREE,
nullable: false,
})
indexType?: IndexType;
}

View File

@@ -1,9 +1,11 @@
import { isPlainObject } from '@nestjs/common/utils/shared.utils';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { ObjectMetadataMapItem } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { getCompositeFieldMetadataCollection } from 'src/engine/twenty-orm/utils/get-composite-field-metadata-collection';
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
import { capitalize } from 'src/utils/capitalize';
export function formatData<T>(
data: T,
@@ -17,49 +19,70 @@ export function formatData<T>(
return data.map((item) => formatData(item, objectMetadata)) as T;
}
const compositeFieldMetadataCollection =
getCompositeFieldMetadataCollection(objectMetadata);
const compositeFieldMetadataMap = new Map(
compositeFieldMetadataCollection.map((fieldMetadata) => [
fieldMetadata.name,
fieldMetadata,
]),
);
const newData: object = {};
const newData: Record<string, any> = {};
for (const [key, value] of Object.entries(data)) {
const fieldMetadata = compositeFieldMetadataMap.get(key);
const fieldMetadata = objectMetadata.fields[key];
if (!fieldMetadata) {
if (isPlainObject(value)) {
newData[key] = formatData(value, objectMetadata);
} else {
newData[key] = value;
}
continue;
}
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
if (!compositeType) {
continue;
}
for (const compositeProperty of compositeType.properties) {
const compositeKey = computeCompositeColumnName(
fieldMetadata.name,
compositeProperty,
throw new Error(
`Field metadata for field "${key}" is missing in object metadata`,
);
const value = data?.[key]?.[compositeProperty.name];
}
if (value === undefined || value === null) {
continue;
}
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
const formattedCompositeField = formatCompositeField(
value,
fieldMetadata,
);
newData[compositeKey] = data[key][compositeProperty.name];
Object.assign(newData, formattedCompositeField);
} else {
newData[key] = formatFieldMetadataValue(value, fieldMetadata);
}
}
return newData as T;
}
function formatCompositeField(
value: any,
fieldMetadata: FieldMetadataInterface,
): Record<string, any> {
const compositeType = compositeTypeDefinitions.get(
fieldMetadata.type as CompositeFieldMetadataType,
);
if (!compositeType) {
throw new Error(
`Composite type definition not found for type: ${fieldMetadata.type}`,
);
}
const formattedCompositeField: Record<string, any> = {};
for (const property of compositeType.properties) {
const subFieldKey = property.name;
const fullFieldName = `${fieldMetadata.name}${capitalize(subFieldKey)}`;
if (value && value[subFieldKey] !== undefined) {
formattedCompositeField[fullFieldName] = formatFieldMetadataValue(
value[subFieldKey],
property as unknown as FieldMetadataInterface,
);
}
}
return formattedCompositeField;
}
function formatFieldMetadataValue(
value: any,
fieldMetadata: FieldMetadataInterface,
) {
if (fieldMetadata.type === FieldMetadataType.RAW_JSON) {
return JSON.parse(value as string);
}
return value;
}

View File

@@ -1,6 +1,9 @@
import { isPlainObject } from '@nestjs/common/utils/shared.utils';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import {
@@ -81,9 +84,15 @@ export function formatResult<T>(
if (!compositePropertyArgs && !relationMetadata) {
if (isPlainObject(value)) {
newData[key] = formatResult(value, objectMetadata, objectMetadataMap);
} else if (objectMetadata.fields[key]) {
newData[key] = formatFieldMetadataValue(
value,
objectMetadata.fields[key],
);
} else {
newData[key] = value;
}
continue;
}
@@ -129,3 +138,18 @@ export function formatResult<T>(
return newData as T;
}
function formatFieldMetadataValue(
value: any,
fieldMetadata: FieldMetadataInterface,
) {
if (
typeof value === 'string' &&
(fieldMetadata.type === FieldMetadataType.MULTI_SELECT ||
fieldMetadata.type === FieldMetadataType.ARRAY)
) {
return value.replace(/{|}/g, '').split(',');
}
return value;
}