Revert "Revert "[4/n]: migrate the RESTAPI GET /rest/* to use TwentyORM direc…" (#11349)
This commit is contained in:
2
Makefile
2
Makefile
@ -20,4 +20,4 @@ redis-on-docker:
|
||||
docker run -d --name twenty_redis -p 6379:6379 redis/redis-stack-server:latest
|
||||
|
||||
clickhouse-on-docker:
|
||||
docker run -d --name twenty_clickhouse -p 8123:8123 -p 9000:9000 -e CLICKHOUSE_PASSWORD=devPassword clickhouse/clickhouse-server:latest
|
||||
docker run -d --name twenty_clickhouse -p 8123:8123 -p 9000:9000 -e CLICKHOUSE_PASSWORD=clickhousePassword clickhouse/clickhouse-server:latest \
|
||||
|
||||
@ -38,6 +38,7 @@ const MIGRATED_REST_METHODS = [
|
||||
RequestMethod.POST,
|
||||
RequestMethod.PATCH,
|
||||
RequestMethod.PUT,
|
||||
RequestMethod.GET,
|
||||
];
|
||||
|
||||
@Module({
|
||||
|
||||
@ -9,6 +9,7 @@ import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
describe('WorkspaceSchemaFactory', () => {
|
||||
let service: WorkspaceSchemaFactory;
|
||||
@ -49,6 +50,10 @@ describe('WorkspaceSchemaFactory', () => {
|
||||
provide: FeatureFlagService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: TwentyConfigService,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
import { WhereExpressionBuilder } from 'typeorm';
|
||||
|
||||
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
|
||||
@ -4,7 +4,6 @@ import {
|
||||
ObjectRecordOrderBy,
|
||||
OrderByDirection,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import {
|
||||
@ -18,14 +17,9 @@ import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspac
|
||||
|
||||
export class GraphqlQueryOrderFieldParser {
|
||||
private fieldMetadataMapByName: FieldMetadataMap;
|
||||
private featureFlagsMap: FeatureFlagMap;
|
||||
|
||||
constructor(
|
||||
fieldMetadataMapByName: FieldMetadataMap,
|
||||
featureFlagsMap: FeatureFlagMap,
|
||||
) {
|
||||
constructor(fieldMetadataMapByName: FieldMetadataMap) {
|
||||
this.fieldMetadataMapByName = fieldMetadataMapByName;
|
||||
this.featureFlagsMap = featureFlagsMap;
|
||||
}
|
||||
|
||||
parse(
|
||||
|
||||
@ -21,6 +21,10 @@ import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metada
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
|
||||
export class GraphqlQueryParser {
|
||||
private fieldMetadataMapByName: FieldMetadataMap;
|
||||
@ -47,7 +51,6 @@ export class GraphqlQueryParser {
|
||||
);
|
||||
this.orderFieldParser = new GraphqlQueryOrderFieldParser(
|
||||
this.fieldMetadataMapByName,
|
||||
featureFlagsMap,
|
||||
);
|
||||
}
|
||||
|
||||
@ -125,8 +128,9 @@ export class GraphqlQueryParser {
|
||||
)?.fieldsByName;
|
||||
|
||||
if (!parentFields) {
|
||||
throw new Error(
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Could not find object metadata for ${parentObjectMetadata.nameSingular}`,
|
||||
GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -6,6 +6,8 @@ import { GraphQLSchema, printSchema } from 'graphql';
|
||||
import { gql } from 'graphql-tag';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
@ -21,6 +23,7 @@ import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceSchemaFactory {
|
||||
@ -32,6 +35,7 @@ export class WorkspaceSchemaFactory {
|
||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
) {}
|
||||
|
||||
async createGraphQLSchema(authContext: AuthContext): Promise<GraphQLSchema> {
|
||||
@ -44,7 +48,10 @@ export class WorkspaceSchemaFactory {
|
||||
authContext.workspace.id,
|
||||
);
|
||||
|
||||
if (isNewRelationEnabled) {
|
||||
if (
|
||||
isNewRelationEnabled &&
|
||||
this.twentyConfigService.get('NODE_ENV') !== NodeEnvironment.test
|
||||
) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
chalk.yellow('🚧 New relation schema generation is enabled 🚧'),
|
||||
|
||||
@ -22,6 +22,7 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
|
||||
@Controller('rest/*')
|
||||
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
|
||||
@UseFilters(RestApiExceptionFilter)
|
||||
export class RestApiCoreController {
|
||||
constructor(
|
||||
private readonly restApiCoreService: RestApiCoreService,
|
||||
@ -38,15 +39,12 @@ export class RestApiCoreController {
|
||||
|
||||
@Get()
|
||||
async handleApiGet(@Req() request: Request, @Res() res: Response) {
|
||||
const result = await this.restApiCoreService.get(request);
|
||||
const result = await this.restApiCoreServiceV2.get(request);
|
||||
|
||||
res.status(200).send(cleanGraphQLResponse(result.data.data));
|
||||
res.status(200).send(result);
|
||||
}
|
||||
|
||||
@Delete()
|
||||
// We should move this exception filter to RestApiCoreController class level
|
||||
// when all endpoints are migrated to v2
|
||||
@UseFilters(RestApiExceptionFilter)
|
||||
async handleApiDelete(@Req() request: Request, @Res() res: Response) {
|
||||
const result = await this.restApiCoreServiceV2.delete(request);
|
||||
|
||||
@ -54,7 +52,6 @@ export class RestApiCoreController {
|
||||
}
|
||||
|
||||
@Post()
|
||||
@UseFilters(RestApiExceptionFilter)
|
||||
async handleApiPost(@Req() request: Request, @Res() res: Response) {
|
||||
const result = await this.restApiCoreServiceV2.createOne(request);
|
||||
|
||||
@ -73,7 +70,6 @@ export class RestApiCoreController {
|
||||
// We keep it to avoid a breaking change since it initially used PUT instead of PATCH,
|
||||
// and because the PUT verb is often used as a PATCH.
|
||||
@Put()
|
||||
@UseFilters(RestApiExceptionFilter)
|
||||
async handleApiPut(@Req() request: Request, @Res() res: Response) {
|
||||
const result = await this.restApiCoreServiceV2.update(request);
|
||||
|
||||
|
||||
@ -0,0 +1,58 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiCreateOneHandler extends RestApiBaseHandler {
|
||||
async handle(request: Request) {
|
||||
const { objectMetadataNameSingular, objectMetadata, repository } =
|
||||
await this.getRepositoryAndMetadataOrFail(request);
|
||||
|
||||
const overriddenBody = await this.recordInputTransformerService.process({
|
||||
recordInput: request.body,
|
||||
objectMetadataMapItem: objectMetadata.objectMetadataMapItem,
|
||||
});
|
||||
|
||||
const recordExists =
|
||||
isDefined(overriddenBody.id) &&
|
||||
(await repository.exists({
|
||||
where: {
|
||||
id: overriddenBody.id,
|
||||
},
|
||||
}));
|
||||
|
||||
if (recordExists) {
|
||||
throw new BadRequestException('Record already exists');
|
||||
}
|
||||
|
||||
const createdRecord = await repository.save(overriddenBody);
|
||||
|
||||
this.apiEventEmitterService.emitCreateEvents(
|
||||
[createdRecord],
|
||||
this.getAuthContextFromRequest(request),
|
||||
objectMetadata.objectMetadataMapItem,
|
||||
);
|
||||
|
||||
const records = await this.getRecord({
|
||||
recordIds: [createdRecord.id],
|
||||
repository,
|
||||
objectMetadata,
|
||||
depth: this.depthInputFactory.create(request),
|
||||
});
|
||||
|
||||
const record = records[0];
|
||||
|
||||
if (!isDefined(record)) {
|
||||
throw new Error('Created record not found');
|
||||
}
|
||||
|
||||
return this.formatResult({
|
||||
operation: 'create',
|
||||
objectNameSingular: objectMetadataNameSingular,
|
||||
data: record,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiDeleteOneHandler extends RestApiBaseHandler {
|
||||
async handle(request: Request) {
|
||||
const { id: recordId } = parseCorePath(request);
|
||||
|
||||
if (!recordId) {
|
||||
throw new BadRequestException('Record ID not found');
|
||||
}
|
||||
|
||||
const { objectMetadataNameSingular, objectMetadata, repository } =
|
||||
await this.getRepositoryAndMetadataOrFail(request);
|
||||
const recordToDelete = await repository.findOneOrFail({
|
||||
where: { id: recordId },
|
||||
});
|
||||
|
||||
await repository.delete(recordId);
|
||||
|
||||
this.apiEventEmitterService.emitDestroyEvents(
|
||||
[recordToDelete],
|
||||
this.getAuthContextFromRequest(request),
|
||||
objectMetadata.objectMetadataMapItem,
|
||||
);
|
||||
|
||||
return this.formatResult({
|
||||
operation: 'delete',
|
||||
objectNameSingular: objectMetadataNameSingular,
|
||||
data: {
|
||||
id: recordToDelete.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiGetManyHandler extends RestApiBaseHandler {
|
||||
async handle(request: Request) {
|
||||
const {
|
||||
objectMetadataNameSingular,
|
||||
objectMetadataNamePlural,
|
||||
repository,
|
||||
dataSource,
|
||||
objectMetadata,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
} = await this.getRepositoryAndMetadataOrFail(request);
|
||||
|
||||
const { records, isForwardPagination, hasMoreRecords, totalCount } =
|
||||
await this.findRecords({
|
||||
request,
|
||||
repository,
|
||||
dataSource,
|
||||
objectMetadata,
|
||||
objectMetadataNameSingular,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
});
|
||||
|
||||
return this.formatPaginatedResult(
|
||||
records,
|
||||
objectMetadataNamePlural,
|
||||
isForwardPagination,
|
||||
hasMoreRecords,
|
||||
totalCount,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiGetOneHandler extends RestApiBaseHandler {
|
||||
async handle(request: Request) {
|
||||
const { id: recordId } = parseCorePath(request);
|
||||
|
||||
if (!isDefined(recordId)) {
|
||||
throw new BadRequestException(
|
||||
'No recordId provided in rest api get one query',
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
objectMetadataNameSingular,
|
||||
repository,
|
||||
dataSource,
|
||||
objectMetadata,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
} = await this.getRepositoryAndMetadataOrFail(request);
|
||||
|
||||
const { records } = await this.findRecords({
|
||||
request,
|
||||
recordId,
|
||||
repository,
|
||||
dataSource,
|
||||
objectMetadata,
|
||||
objectMetadataNameSingular,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
});
|
||||
|
||||
const record = records?.[0];
|
||||
|
||||
if (!isDefined(record)) {
|
||||
throw new BadRequestException('Record not found');
|
||||
}
|
||||
|
||||
return this.formatResult({
|
||||
operation: 'findOne',
|
||||
objectNameSingular: objectMetadataNameSingular,
|
||||
data: record,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiUpdateOneHandler extends RestApiBaseHandler {
|
||||
async handle(request: Request) {
|
||||
const { id: recordId } = parseCorePath(request);
|
||||
|
||||
if (!recordId) {
|
||||
throw new BadRequestException('Record ID not found');
|
||||
}
|
||||
|
||||
const { objectMetadataNameSingular, objectMetadata, repository } =
|
||||
await this.getRepositoryAndMetadataOrFail(request);
|
||||
|
||||
const recordToUpdate = await repository.findOneOrFail({
|
||||
where: { id: recordId },
|
||||
});
|
||||
|
||||
const overriddenBody = await this.recordInputTransformerService.process({
|
||||
recordInput: request.body,
|
||||
objectMetadataMapItem: objectMetadata.objectMetadataMapItem,
|
||||
});
|
||||
|
||||
const updatedRecord = await repository.save({
|
||||
...recordToUpdate,
|
||||
...overriddenBody,
|
||||
});
|
||||
|
||||
this.apiEventEmitterService.emitUpdateEvents(
|
||||
[recordToUpdate],
|
||||
[updatedRecord],
|
||||
Object.keys(request.body),
|
||||
this.getAuthContextFromRequest(request),
|
||||
objectMetadata.objectMetadataMapItem,
|
||||
);
|
||||
|
||||
const records = await this.getRecord({
|
||||
recordIds: [updatedRecord.id],
|
||||
repository,
|
||||
objectMetadata,
|
||||
depth: this.depthInputFactory.create(request),
|
||||
});
|
||||
|
||||
const record = records[0];
|
||||
|
||||
if (!isDefined(record)) {
|
||||
throw new Error('Updated record not found');
|
||||
}
|
||||
|
||||
return this.formatResult({
|
||||
operation: 'update',
|
||||
objectNameSingular: objectMetadataNameSingular,
|
||||
data: record,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,416 @@
|
||||
import { BadRequestException, Inject } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { In, ObjectLiteral, SelectQueryBuilder } from 'typeorm';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
ObjectRecord,
|
||||
OrderByDirection,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { CoreQueryBuilderFactory } from 'src/engine/api/rest/core/query-builder/core-query-builder.factory';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { GetVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/get-variables.factory';
|
||||
import {
|
||||
Depth,
|
||||
DepthInputFactory,
|
||||
MAX_DEPTH,
|
||||
} from 'src/engine/api/rest/input-factories/depth-input.factory';
|
||||
import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
|
||||
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
|
||||
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
|
||||
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
|
||||
import { formatResult as formatGetManyData } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { QueryVariables } from 'src/engine/api/rest/core/types/query-variables.type';
|
||||
|
||||
export interface PageInfo {
|
||||
hasNextPage?: boolean;
|
||||
hasPreviousPage?: boolean;
|
||||
startCursor: string | null;
|
||||
endCursor: string | null;
|
||||
}
|
||||
|
||||
interface FormatResultParams<T> {
|
||||
operation: 'delete' | 'create' | 'update' | 'findOne' | 'findMany';
|
||||
objectNameSingular?: string;
|
||||
objectNamePlural?: string;
|
||||
data: T;
|
||||
pageInfo?: PageInfo;
|
||||
totalCount?: number;
|
||||
}
|
||||
|
||||
export interface FormatResult {
|
||||
data: {
|
||||
[operation: string]: object;
|
||||
};
|
||||
pageInfo?: PageInfo;
|
||||
totalCount?: number;
|
||||
}
|
||||
|
||||
export abstract class RestApiBaseHandler {
|
||||
@Inject()
|
||||
protected readonly recordInputTransformerService: RecordInputTransformerService;
|
||||
@Inject()
|
||||
protected readonly coreQueryBuilderFactory: CoreQueryBuilderFactory;
|
||||
@Inject()
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager;
|
||||
@Inject()
|
||||
protected readonly getVariablesFactory: GetVariablesFactory;
|
||||
@Inject()
|
||||
protected readonly depthInputFactory: DepthInputFactory;
|
||||
@Inject()
|
||||
protected readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService;
|
||||
@Inject()
|
||||
protected readonly apiEventEmitterService: ApiEventEmitterService;
|
||||
|
||||
protected abstract handle(request: Request): Promise<FormatResult>;
|
||||
|
||||
public async getRepositoryAndMetadataOrFail(request: Request) {
|
||||
const { workspace, apiKey, userWorkspaceId } = request;
|
||||
const { object: parsedObject } = parseCorePath(request);
|
||||
|
||||
const objectMetadata = await this.coreQueryBuilderFactory.getObjectMetadata(
|
||||
request,
|
||||
parsedObject,
|
||||
);
|
||||
|
||||
if (!objectMetadata) {
|
||||
throw new BadRequestException('Object metadata not found');
|
||||
}
|
||||
|
||||
if (!workspace?.id) {
|
||||
throw new BadRequestException('Workspace not found');
|
||||
}
|
||||
|
||||
const dataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
|
||||
workspaceId: workspace.id,
|
||||
shouldFailIfMetadataNotFound: false,
|
||||
});
|
||||
|
||||
const objectMetadataNameSingular =
|
||||
objectMetadata.objectMetadataMapItem.nameSingular;
|
||||
|
||||
const objectMetadataItemWithFieldsMaps =
|
||||
getObjectMetadataMapItemByNameSingular(
|
||||
objectMetadata.objectMetadataMaps,
|
||||
objectMetadataNameSingular,
|
||||
);
|
||||
|
||||
const shouldBypassPermissionChecks = !!apiKey;
|
||||
|
||||
const roleId =
|
||||
await this.workspacePermissionsCacheService.getRoleIdFromUserWorkspaceId({
|
||||
workspaceId: workspace.id,
|
||||
userWorkspaceId,
|
||||
});
|
||||
|
||||
const repository = dataSource.getRepository<ObjectRecord>(
|
||||
objectMetadataNameSingular,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
);
|
||||
|
||||
return {
|
||||
objectMetadataNameSingular,
|
||||
objectMetadataNamePlural: objectMetadata.objectMetadataMapItem.namePlural,
|
||||
objectMetadata,
|
||||
repository,
|
||||
dataSource,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
};
|
||||
}
|
||||
|
||||
getRelations({
|
||||
objectMetadata,
|
||||
depth,
|
||||
}: {
|
||||
objectMetadata: {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
};
|
||||
depth: Depth | undefined;
|
||||
}) {
|
||||
if (!isDefined(depth) || depth === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const relations: string[] = [];
|
||||
|
||||
objectMetadata.objectMetadataMapItem.fields.forEach((field) => {
|
||||
if (field.type === FieldMetadataType.RELATION) {
|
||||
if (
|
||||
depth === MAX_DEPTH &&
|
||||
isDefined(field.relationTargetObjectMetadataId)
|
||||
) {
|
||||
const relationTargetObjectMetadata =
|
||||
objectMetadata.objectMetadataMaps.byId[
|
||||
field.relationTargetObjectMetadataId
|
||||
];
|
||||
const depth2Relations = this.getRelations({
|
||||
objectMetadata: {
|
||||
objectMetadataMaps: objectMetadata.objectMetadataMaps,
|
||||
objectMetadataMapItem: relationTargetObjectMetadata,
|
||||
},
|
||||
depth: 1,
|
||||
});
|
||||
|
||||
depth2Relations.forEach((depth2Relation) => {
|
||||
relations.push(`${field.name}.${depth2Relation}`);
|
||||
});
|
||||
} else {
|
||||
relations.push(`${field.name}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return relations;
|
||||
}
|
||||
|
||||
async getRecord({
|
||||
recordIds,
|
||||
repository,
|
||||
objectMetadata,
|
||||
depth,
|
||||
}: {
|
||||
recordIds: string[];
|
||||
repository: WorkspaceRepository<ObjectLiteral>;
|
||||
objectMetadata: {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
};
|
||||
depth: Depth | undefined;
|
||||
}) {
|
||||
const relations = this.getRelations({
|
||||
objectMetadata,
|
||||
depth: depth,
|
||||
});
|
||||
|
||||
const unorderedRecords = await repository.find({
|
||||
where: { id: In(recordIds) },
|
||||
relations,
|
||||
});
|
||||
|
||||
const recordMap = new Map(unorderedRecords.map((r) => [r.id, r]));
|
||||
|
||||
const orderedRecords = recordIds.map((id) => recordMap.get(id));
|
||||
|
||||
return orderedRecords;
|
||||
}
|
||||
|
||||
public getAuthContextFromRequest(request: Request): AuthContext {
|
||||
return {
|
||||
user: request.user,
|
||||
workspace: request.workspace,
|
||||
apiKey: request.apiKey,
|
||||
workspaceMemberId: request.workspaceMemberId,
|
||||
userWorkspaceId: request.userWorkspaceId,
|
||||
};
|
||||
}
|
||||
|
||||
public formatResult<T>({
|
||||
operation,
|
||||
objectNameSingular,
|
||||
objectNamePlural,
|
||||
data,
|
||||
pageInfo,
|
||||
totalCount,
|
||||
}: FormatResultParams<T>) {
|
||||
let prefix: string;
|
||||
|
||||
if (operation === 'findOne') {
|
||||
prefix = objectNameSingular || '';
|
||||
} else if (operation === 'findMany') {
|
||||
prefix = objectNamePlural || '';
|
||||
} else {
|
||||
prefix = operation + capitalize(objectNameSingular || '');
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
[prefix]: data,
|
||||
},
|
||||
...(isDefined(pageInfo) ? { pageInfo } : {}),
|
||||
...(isDefined(totalCount) ? { totalCount } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
formatPaginatedResult(
|
||||
finalRecords: any[],
|
||||
objectMetadataNamePlural: string,
|
||||
isForwardPagination: boolean,
|
||||
hasMoreRecords: boolean,
|
||||
totalCount: number,
|
||||
) {
|
||||
const hasPreviousPage = !isForwardPagination && hasMoreRecords;
|
||||
|
||||
return this.formatResult({
|
||||
operation: 'findMany',
|
||||
objectNamePlural: objectMetadataNamePlural,
|
||||
data: isForwardPagination ? finalRecords : finalRecords.reverse(),
|
||||
pageInfo: {
|
||||
hasNextPage: isForwardPagination && hasMoreRecords,
|
||||
...(hasPreviousPage ? { hasPreviousPage } : {}),
|
||||
startCursor:
|
||||
finalRecords.length > 0
|
||||
? Buffer.from(JSON.stringify({ id: finalRecords[0].id })).toString(
|
||||
'base64',
|
||||
)
|
||||
: null,
|
||||
endCursor:
|
||||
finalRecords.length > 0
|
||||
? Buffer.from(
|
||||
JSON.stringify({
|
||||
id: finalRecords[finalRecords.length - 1].id,
|
||||
}),
|
||||
).toString('base64')
|
||||
: null,
|
||||
},
|
||||
totalCount,
|
||||
});
|
||||
}
|
||||
|
||||
async findRecords({
|
||||
request,
|
||||
recordId,
|
||||
repository,
|
||||
dataSource,
|
||||
objectMetadata,
|
||||
objectMetadataNameSingular,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
}: {
|
||||
request: Request;
|
||||
recordId?: string;
|
||||
repository: WorkspaceRepository<ObjectLiteral>;
|
||||
dataSource: WorkspaceDataSource;
|
||||
objectMetadata: any;
|
||||
objectMetadataNameSingular: string;
|
||||
objectMetadataItemWithFieldsMaps:
|
||||
| ObjectMetadataItemWithFieldMaps
|
||||
| undefined;
|
||||
}) {
|
||||
const qb = repository.createQueryBuilder(objectMetadataNameSingular);
|
||||
|
||||
const inputs = this.getVariablesFactory.create(
|
||||
recordId,
|
||||
request,
|
||||
objectMetadata,
|
||||
);
|
||||
|
||||
const fieldMetadataMapByName =
|
||||
objectMetadataItemWithFieldsMaps?.fieldsByName || {};
|
||||
const fieldMetadataMapByJoinColumnName =
|
||||
objectMetadataItemWithFieldsMaps?.fieldsByJoinColumnName || {};
|
||||
|
||||
const graphqlQueryParser = new GraphqlQueryParser(
|
||||
fieldMetadataMapByName,
|
||||
fieldMetadataMapByJoinColumnName,
|
||||
objectMetadata.objectMetadataMaps,
|
||||
dataSource.featureFlagMap,
|
||||
);
|
||||
|
||||
const filters = this.computeFilters(inputs);
|
||||
|
||||
let selectQueryBuilder = isDefined(filters)
|
||||
? graphqlQueryParser.applyFilterToBuilder(
|
||||
qb,
|
||||
objectMetadataNameSingular,
|
||||
filters,
|
||||
)
|
||||
: qb;
|
||||
|
||||
const totalCount = await this.getTotalCount(selectQueryBuilder);
|
||||
|
||||
const isForwardPagination = !inputs.endingBefore;
|
||||
|
||||
selectQueryBuilder = graphqlQueryParser.applyOrderToBuilder(
|
||||
selectQueryBuilder,
|
||||
[...(inputs.orderBy || []), { id: OrderByDirection.AscNullsFirst }],
|
||||
objectMetadataNameSingular,
|
||||
isForwardPagination,
|
||||
);
|
||||
|
||||
if (inputs.first) {
|
||||
selectQueryBuilder = selectQueryBuilder.limit(inputs.first);
|
||||
}
|
||||
|
||||
if (inputs.last) {
|
||||
selectQueryBuilder = selectQueryBuilder.limit(inputs.last);
|
||||
}
|
||||
|
||||
const recordIds = await selectQueryBuilder
|
||||
.select(`${objectMetadataNameSingular}.id`)
|
||||
.getMany();
|
||||
|
||||
const records = await this.getRecord({
|
||||
recordIds: recordIds.map((record) => record.id),
|
||||
repository,
|
||||
objectMetadata,
|
||||
depth: this.depthInputFactory.create(request),
|
||||
});
|
||||
|
||||
const hasMoreRecords = records.length < totalCount;
|
||||
|
||||
return {
|
||||
records: formatGetManyData<ObjectLiteral[]>(
|
||||
records,
|
||||
objectMetadataItemWithFieldsMaps as any,
|
||||
objectMetadata.objectMetadataMaps,
|
||||
dataSource.featureFlagMap[FeatureFlagKey.IsNewRelationEnabled],
|
||||
),
|
||||
totalCount,
|
||||
hasMoreRecords,
|
||||
isForwardPagination,
|
||||
};
|
||||
}
|
||||
|
||||
async getTotalCount(
|
||||
query: SelectQueryBuilder<ObjectLiteral>,
|
||||
): Promise<number> {
|
||||
const countQuery = query.clone();
|
||||
|
||||
return await countQuery.getCount();
|
||||
}
|
||||
|
||||
computeFilters(inputs: QueryVariables) {
|
||||
let appliedFilters = inputs.filter;
|
||||
|
||||
if (inputs.startingAfter) {
|
||||
appliedFilters = {
|
||||
and: [
|
||||
appliedFilters || {},
|
||||
{ id: { gt: this.parseCursor(inputs.startingAfter).id } },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (inputs.endingBefore) {
|
||||
appliedFilters = {
|
||||
and: [
|
||||
appliedFilters || {},
|
||||
{ id: { lt: this.parseCursor(inputs.endingBefore).id } },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return appliedFilters;
|
||||
}
|
||||
|
||||
private parseCursor = (cursor: string) => {
|
||||
try {
|
||||
return JSON.parse(Buffer.from(cursor ?? '', 'base64').toString());
|
||||
} catch (error) {
|
||||
throw new BadRequestException(`Invalid cursor: ${cursor}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -3,17 +3,8 @@ import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
|
||||
import { CreateManyQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/create-many-query.factory';
|
||||
import { CreateOneQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/create-one-query.factory';
|
||||
import { CreateVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/create-variables.factory';
|
||||
import { DeleteQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/delete-query.factory';
|
||||
import { DeleteVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/delete-variables.factory';
|
||||
import { FindDuplicatesQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/find-duplicates-query.factory';
|
||||
import { FindDuplicatesVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/find-duplicates-variables.factory';
|
||||
import { FindManyQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/find-many-query.factory';
|
||||
import { FindOneQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/find-one-query.factory';
|
||||
import { GetVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/get-variables.factory';
|
||||
import { UpdateQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/update-query.factory';
|
||||
import { UpdateVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/update-variables.factory';
|
||||
import { computeDepth } from 'src/engine/api/rest/core/query-builder/utils/compute-depth.utils';
|
||||
import { parseCoreBatchPath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-batch-path.utils';
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
@ -26,21 +17,14 @@ import { getObjectMetadataMapItemByNamePlural } from 'src/engine/metadata-module
|
||||
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import { CreateVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/create-variables.factory';
|
||||
|
||||
@Injectable()
|
||||
export class CoreQueryBuilderFactory {
|
||||
constructor(
|
||||
private readonly deleteQueryFactory: DeleteQueryFactory,
|
||||
private readonly createOneQueryFactory: CreateOneQueryFactory,
|
||||
private readonly createManyQueryFactory: CreateManyQueryFactory,
|
||||
private readonly updateQueryFactory: UpdateQueryFactory,
|
||||
private readonly findOneQueryFactory: FindOneQueryFactory,
|
||||
private readonly findManyQueryFactory: FindManyQueryFactory,
|
||||
private readonly findDuplicatesQueryFactory: FindDuplicatesQueryFactory,
|
||||
private readonly deleteVariablesFactory: DeleteVariablesFactory,
|
||||
private readonly createVariablesFactory: CreateVariablesFactory,
|
||||
private readonly updateVariablesFactory: UpdateVariablesFactory,
|
||||
private readonly getVariablesFactory: GetVariablesFactory,
|
||||
private readonly findDuplicatesVariablesFactory: FindDuplicatesVariablesFactory,
|
||||
private readonly accessTokenService: AccessTokenService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
@ -113,38 +97,6 @@ export class CoreQueryBuilderFactory {
|
||||
};
|
||||
}
|
||||
|
||||
async delete(request: Request): Promise<Query> {
|
||||
const { object: parsedObject } = parseCorePath(request);
|
||||
const objectMetadata = await this.getObjectMetadata(request, parsedObject);
|
||||
|
||||
const { id } = parseCorePath(request);
|
||||
|
||||
if (!id) {
|
||||
throw new BadRequestException(
|
||||
`delete ${objectMetadata.objectMetadataMapItem.nameSingular} query invalid. Id missing. eg: /rest/${objectMetadata.objectMetadataMapItem.namePlural}/0d4389ef-ea9c-4ae8-ada1-1cddc440fb56`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
query: this.deleteQueryFactory.create(
|
||||
objectMetadata.objectMetadataMapItem,
|
||||
),
|
||||
variables: this.deleteVariablesFactory.create(id),
|
||||
};
|
||||
}
|
||||
|
||||
async createOne(request: Request): Promise<Query> {
|
||||
const { object: parsedObject } = parseCorePath(request);
|
||||
const objectMetadata = await this.getObjectMetadata(request, parsedObject);
|
||||
|
||||
const depth = computeDepth(request);
|
||||
|
||||
return {
|
||||
query: this.createOneQueryFactory.create(objectMetadata, depth),
|
||||
variables: this.createVariablesFactory.create(request),
|
||||
};
|
||||
}
|
||||
|
||||
async createMany(request: Request): Promise<Query> {
|
||||
const { object: parsedObject } = parseCoreBatchPath(request);
|
||||
const objectMetadata = await this.getObjectMetadata(request, parsedObject);
|
||||
@ -156,42 +108,6 @@ export class CoreQueryBuilderFactory {
|
||||
};
|
||||
}
|
||||
|
||||
async update(request: Request): Promise<Query> {
|
||||
const { object: parsedObject } = parseCorePath(request);
|
||||
const objectMetadata = await this.getObjectMetadata(request, parsedObject);
|
||||
|
||||
const depth = computeDepth(request);
|
||||
|
||||
const { id } = parseCorePath(request);
|
||||
|
||||
if (!id) {
|
||||
throw new BadRequestException(
|
||||
`update ${objectMetadata.objectMetadataMapItem.nameSingular} query invalid. Id missing. eg: /rest/${objectMetadata.objectMetadataMapItem.namePlural}/0d4389ef-ea9c-4ae8-ada1-1cddc440fb56`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
query: this.updateQueryFactory.create(objectMetadata, depth),
|
||||
variables: this.updateVariablesFactory.create(id, request),
|
||||
};
|
||||
}
|
||||
|
||||
async get(request: Request): Promise<Query> {
|
||||
const { object: parsedObject } = parseCorePath(request);
|
||||
const objectMetadata = await this.getObjectMetadata(request, parsedObject);
|
||||
|
||||
const depth = computeDepth(request);
|
||||
|
||||
const { id } = parseCorePath(request);
|
||||
|
||||
return {
|
||||
query: id
|
||||
? this.findOneQueryFactory.create(objectMetadata, depth)
|
||||
: this.findManyQueryFactory.create(objectMetadata, depth),
|
||||
variables: this.getVariablesFactory.create(id, request, objectMetadata),
|
||||
};
|
||||
}
|
||||
|
||||
async findDuplicates(request: Request): Promise<Query> {
|
||||
const { object: parsedObject } = parseCorePath(request);
|
||||
const objectMetadata = await this.getObjectMetadata(request, parsedObject);
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
|
||||
@Injectable()
|
||||
export class CreateOneQueryFactory {
|
||||
create(
|
||||
objectMetadata: {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
},
|
||||
depth?: number,
|
||||
): string {
|
||||
const objectNameSingular = capitalize(
|
||||
objectMetadata.objectMetadataMapItem.nameSingular,
|
||||
);
|
||||
|
||||
return `
|
||||
mutation Create${objectNameSingular}($data: ${objectNameSingular}CreateInput!) {
|
||||
create${objectNameSingular}(data: $data) {
|
||||
id
|
||||
${objectMetadata.objectMetadataMapItem.fields
|
||||
.map((field) =>
|
||||
mapFieldMetadataToGraphqlQuery(
|
||||
objectMetadata.objectMetadataMaps,
|
||||
field,
|
||||
depth,
|
||||
),
|
||||
)
|
||||
.join('\n')}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteQueryFactory {
|
||||
create(objectMetadataMapItem: ObjectMetadataItemWithFieldMaps): string {
|
||||
const objectNameSingular = capitalize(objectMetadataMapItem.nameSingular);
|
||||
|
||||
return `
|
||||
mutation Delete${objectNameSingular}($id: ID!) {
|
||||
delete${objectNameSingular}(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { QueryVariables } from 'src/engine/api/rest/core/types/query-variables.type';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteVariablesFactory {
|
||||
create(id: string): QueryVariables {
|
||||
return {
|
||||
id,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,28 +1,14 @@
|
||||
import { CreateManyQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/create-many-query.factory';
|
||||
import { CreateOneQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/create-one-query.factory';
|
||||
import { CreateVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/create-variables.factory';
|
||||
import { DeleteQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/delete-query.factory';
|
||||
import { DeleteVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/delete-variables.factory';
|
||||
import { FindDuplicatesQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/find-duplicates-query.factory';
|
||||
import { FindDuplicatesVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/find-duplicates-variables.factory';
|
||||
import { FindManyQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/find-many-query.factory';
|
||||
import { FindOneQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/find-one-query.factory';
|
||||
import { GetVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/get-variables.factory';
|
||||
import { UpdateQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/update-query.factory';
|
||||
import { UpdateVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/update-variables.factory';
|
||||
import { CreateVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/create-variables.factory';
|
||||
import { inputFactories } from 'src/engine/api/rest/input-factories/factories';
|
||||
|
||||
export const coreQueryBuilderFactories = [
|
||||
DeleteQueryFactory,
|
||||
CreateOneQueryFactory,
|
||||
CreateManyQueryFactory,
|
||||
UpdateQueryFactory,
|
||||
FindOneQueryFactory,
|
||||
FindManyQueryFactory,
|
||||
FindDuplicatesQueryFactory,
|
||||
DeleteVariablesFactory,
|
||||
CreateVariablesFactory,
|
||||
UpdateVariablesFactory,
|
||||
GetVariablesFactory,
|
||||
FindDuplicatesVariablesFactory,
|
||||
...inputFactories,
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
|
||||
@Injectable()
|
||||
export class FindManyQueryFactory {
|
||||
create(
|
||||
objectMetadata: {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
},
|
||||
depth?: number,
|
||||
): string {
|
||||
const objectNameSingular = capitalize(
|
||||
objectMetadata.objectMetadataMapItem.nameSingular,
|
||||
);
|
||||
const objectNamePlural = objectMetadata.objectMetadataMapItem.namePlural;
|
||||
|
||||
return `
|
||||
query FindMany${capitalize(objectNamePlural)}(
|
||||
$filter: ${objectNameSingular}FilterInput,
|
||||
$orderBy: [${objectNameSingular}OrderByInput],
|
||||
$startingAfter: String,
|
||||
$endingBefore: String,
|
||||
$first: Int,
|
||||
$last: Int
|
||||
) {
|
||||
${objectNamePlural}(
|
||||
filter: $filter,
|
||||
orderBy: $orderBy,
|
||||
first: $first,
|
||||
last: $last,
|
||||
after: $startingAfter,
|
||||
before: $endingBefore
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
${objectMetadata.objectMetadataMapItem.fields
|
||||
.map((field) =>
|
||||
mapFieldMetadataToGraphqlQuery(
|
||||
objectMetadata.objectMetadataMaps,
|
||||
field,
|
||||
depth,
|
||||
),
|
||||
)
|
||||
.join('\n')}
|
||||
}
|
||||
cursor
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
startCursor
|
||||
endCursor
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
|
||||
@Injectable()
|
||||
export class FindOneQueryFactory {
|
||||
create(
|
||||
objectMetadata: {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
},
|
||||
depth?: number,
|
||||
): string {
|
||||
const objectNameSingular =
|
||||
objectMetadata.objectMetadataMapItem.nameSingular;
|
||||
|
||||
return `
|
||||
query FindOne${capitalize(objectNameSingular)}(
|
||||
$filter: ${capitalize(objectNameSingular)}FilterInput!,
|
||||
) {
|
||||
${objectNameSingular}(filter: $filter) {
|
||||
id
|
||||
${objectMetadata.objectMetadataMapItem.fields
|
||||
.map((field) =>
|
||||
mapFieldMetadataToGraphqlQuery(
|
||||
objectMetadata.objectMetadataMaps,
|
||||
field,
|
||||
depth,
|
||||
),
|
||||
)
|
||||
.join('\n')}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -33,13 +33,15 @@ export class GetVariablesFactory {
|
||||
return { filter: { id: { eq: id } } };
|
||||
}
|
||||
|
||||
const filter = this.filterInputFactory.create(request, objectMetadata);
|
||||
const limit = this.limitInputFactory.create(request);
|
||||
const orderBy = this.orderByInputFactory.create(request, objectMetadata);
|
||||
const endingBefore = this.endingBeforeInputFactory.create(request);
|
||||
const startingAfter = this.startingAfterInputFactory.create(request);
|
||||
|
||||
return {
|
||||
filter: this.filterInputFactory.create(request, objectMetadata),
|
||||
orderBy: this.orderByInputFactory.create(request, objectMetadata),
|
||||
filter,
|
||||
orderBy,
|
||||
first: !endingBefore ? limit : undefined,
|
||||
last: endingBefore ? limit : undefined,
|
||||
startingAfter,
|
||||
|
||||
@ -1,40 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
|
||||
@Injectable()
|
||||
export class UpdateQueryFactory {
|
||||
create(
|
||||
objectMetadata: {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
},
|
||||
depth?: number,
|
||||
): string {
|
||||
const objectNameSingular =
|
||||
objectMetadata.objectMetadataMapItem.nameSingular;
|
||||
|
||||
return `
|
||||
mutation Update${capitalize(
|
||||
objectNameSingular,
|
||||
)}($id: ID!, $data: ${capitalize(objectNameSingular)}UpdateInput!) {
|
||||
update${capitalize(objectNameSingular)}(id: $id, data: $data) {
|
||||
id
|
||||
${Object.values(objectMetadata.objectMetadataMapItem.fieldsById)
|
||||
.map((field) =>
|
||||
mapFieldMetadataToGraphqlQuery(
|
||||
objectMetadata.objectMetadataMaps,
|
||||
field,
|
||||
depth,
|
||||
),
|
||||
)
|
||||
.join('\n')}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
|
||||
import { QueryVariables } from 'src/engine/api/rest/core/types/query-variables.type';
|
||||
|
||||
@Injectable()
|
||||
export class UpdateVariablesFactory {
|
||||
create(id: string, request: Request): QueryVariables {
|
||||
return {
|
||||
id,
|
||||
data: request.body,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,197 +1,44 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service';
|
||||
import { CoreQueryBuilderFactory } from 'src/engine/api/rest/core/query-builder/core-query-builder.factory';
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { RestApiDeleteOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-delete-one.handler';
|
||||
import { RestApiCreateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-one.handler';
|
||||
import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler';
|
||||
import { RestApiGetOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-get-one.handler';
|
||||
import { RestApiGetManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-get-many.handler';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiCoreServiceV2 {
|
||||
constructor(
|
||||
private readonly coreQueryBuilderFactory: CoreQueryBuilderFactory,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly recordInputTransformerService: RecordInputTransformerService,
|
||||
protected readonly apiEventEmitterService: ApiEventEmitterService,
|
||||
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
|
||||
private readonly restApiDeleteOneHandler: RestApiDeleteOneHandler,
|
||||
private readonly restApiCreateOneHandler: RestApiCreateOneHandler,
|
||||
private readonly restApiUpdateOneHandler: RestApiUpdateOneHandler,
|
||||
private readonly restApiGetOneHandler: RestApiGetOneHandler,
|
||||
private readonly restApiGetManyHandler: RestApiGetManyHandler,
|
||||
) {}
|
||||
|
||||
async delete(request: Request) {
|
||||
const { id: recordId } = parseCorePath(request);
|
||||
|
||||
if (!recordId) {
|
||||
throw new BadRequestException('Record ID not found');
|
||||
}
|
||||
|
||||
const { objectMetadataNameSingular, objectMetadata, repository } =
|
||||
await this.getRepositoryAndMetadataOrFail(request);
|
||||
const recordToDelete = await repository.findOneOrFail({
|
||||
where: { id: recordId },
|
||||
});
|
||||
|
||||
await repository.delete(recordId);
|
||||
|
||||
this.apiEventEmitterService.emitDestroyEvents(
|
||||
[recordToDelete],
|
||||
this.getAuthContextFromRequest(request),
|
||||
objectMetadata.objectMetadataMapItem,
|
||||
);
|
||||
|
||||
return this.formatResult('delete', objectMetadataNameSingular, {
|
||||
id: recordToDelete.id,
|
||||
});
|
||||
return await this.restApiDeleteOneHandler.handle(request);
|
||||
}
|
||||
|
||||
async createOne(request: Request) {
|
||||
const { objectMetadataNameSingular, objectMetadata, repository } =
|
||||
await this.getRepositoryAndMetadataOrFail(request);
|
||||
|
||||
const overriddenBody = await this.recordInputTransformerService.process({
|
||||
recordInput: request.body,
|
||||
objectMetadataMapItem: objectMetadata.objectMetadataMapItem,
|
||||
});
|
||||
|
||||
const recordExists =
|
||||
isDefined(overriddenBody.id) &&
|
||||
(await repository.exists({
|
||||
where: {
|
||||
id: overriddenBody.id,
|
||||
},
|
||||
}));
|
||||
|
||||
if (recordExists) {
|
||||
throw new BadRequestException('Record already exists');
|
||||
}
|
||||
|
||||
const createdRecord = await repository.save(overriddenBody);
|
||||
|
||||
this.apiEventEmitterService.emitCreateEvents(
|
||||
[createdRecord],
|
||||
this.getAuthContextFromRequest(request),
|
||||
objectMetadata.objectMetadataMapItem,
|
||||
);
|
||||
|
||||
return this.formatResult(
|
||||
'create',
|
||||
objectMetadataNameSingular,
|
||||
createdRecord,
|
||||
);
|
||||
return await this.restApiCreateOneHandler.handle(request);
|
||||
}
|
||||
|
||||
async update(request: Request) {
|
||||
return await this.restApiUpdateOneHandler.handle(request);
|
||||
}
|
||||
|
||||
async get(request: Request) {
|
||||
const { id: recordId } = parseCorePath(request);
|
||||
|
||||
if (!recordId) {
|
||||
throw new BadRequestException('Record ID not found');
|
||||
if (isDefined(recordId)) {
|
||||
return await this.restApiGetOneHandler.handle(request);
|
||||
} else {
|
||||
return await this.restApiGetManyHandler.handle(request);
|
||||
}
|
||||
|
||||
const { objectMetadataNameSingular, objectMetadata, repository } =
|
||||
await this.getRepositoryAndMetadataOrFail(request);
|
||||
|
||||
const recordToUpdate = await repository.findOneOrFail({
|
||||
where: { id: recordId },
|
||||
});
|
||||
|
||||
const overriddenBody = await this.recordInputTransformerService.process({
|
||||
recordInput: request.body,
|
||||
objectMetadataMapItem: objectMetadata.objectMetadataMapItem,
|
||||
});
|
||||
|
||||
const updatedRecord = await repository.save({
|
||||
...recordToUpdate,
|
||||
...overriddenBody,
|
||||
});
|
||||
|
||||
this.apiEventEmitterService.emitUpdateEvents(
|
||||
[recordToUpdate],
|
||||
[updatedRecord],
|
||||
Object.keys(request.body),
|
||||
this.getAuthContextFromRequest(request),
|
||||
objectMetadata.objectMetadataMapItem,
|
||||
);
|
||||
|
||||
return this.formatResult(
|
||||
'update',
|
||||
objectMetadataNameSingular,
|
||||
updatedRecord,
|
||||
);
|
||||
}
|
||||
|
||||
private formatResult<T>(
|
||||
operation: 'delete' | 'create' | 'update' | 'find',
|
||||
objectNameSingular: string,
|
||||
data: T,
|
||||
) {
|
||||
const result = {
|
||||
data: {
|
||||
[operation + capitalize(objectNameSingular)]: data,
|
||||
},
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async getRepositoryAndMetadataOrFail(request: Request) {
|
||||
const { workspace, apiKey, userWorkspaceId } = request;
|
||||
const { object: parsedObject } = parseCorePath(request);
|
||||
|
||||
const objectMetadata = await this.coreQueryBuilderFactory.getObjectMetadata(
|
||||
request,
|
||||
parsedObject,
|
||||
);
|
||||
|
||||
if (!objectMetadata) {
|
||||
throw new BadRequestException('Object metadata not found');
|
||||
}
|
||||
|
||||
if (!workspace?.id) {
|
||||
throw new BadRequestException('Workspace not found');
|
||||
}
|
||||
|
||||
const dataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
|
||||
workspaceId: workspace.id,
|
||||
shouldFailIfMetadataNotFound: false,
|
||||
});
|
||||
|
||||
const objectMetadataNameSingular =
|
||||
objectMetadata.objectMetadataMapItem.nameSingular;
|
||||
|
||||
const shouldBypassPermissionChecks = !!apiKey;
|
||||
|
||||
const roleId =
|
||||
await this.workspacePermissionsCacheService.getRoleIdFromUserWorkspaceId({
|
||||
workspaceId: workspace.id,
|
||||
userWorkspaceId,
|
||||
});
|
||||
|
||||
const repository = dataSource.getRepository<ObjectRecord>(
|
||||
objectMetadataNameSingular,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
);
|
||||
|
||||
return {
|
||||
objectMetadataNameSingular,
|
||||
objectMetadata,
|
||||
repository,
|
||||
};
|
||||
}
|
||||
|
||||
private getAuthContextFromRequest(request: Request): AuthContext {
|
||||
return {
|
||||
user: request.user,
|
||||
workspace: request.workspace,
|
||||
apiKey: request.apiKey,
|
||||
workspaceMemberId: request.workspaceMemberId,
|
||||
userWorkspaceId: request.userWorkspaceId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
|
||||
import { RestApiDeleteOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-delete-one.handler';
|
||||
import { RestApiCreateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-one.handler';
|
||||
import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler';
|
||||
import { RestApiGetOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-get-one.handler';
|
||||
import { RestApiGetManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-get-many.handler';
|
||||
import { CoreQueryBuilderModule } from 'src/engine/api/rest/core/query-builder/core-query-builder.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module';
|
||||
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||
import { RestApiCoreController } from 'src/engine/api/rest/core/controllers/rest-api-core.controller';
|
||||
import { coreQueryBuilderFactories } from 'src/engine/api/rest/core/query-builder/factories/factories';
|
||||
import { RestApiCoreBatchController } from 'src/engine/api/rest/core/controllers/rest-api-core-batch.controller';
|
||||
import { RestApiCoreServiceV2 } from 'src/engine/api/rest/core/rest-api-core-v2.service';
|
||||
import { RestApiCoreService } from 'src/engine/api/rest/core/rest-api-core.service';
|
||||
import { RestApiService } from 'src/engine/api/rest/rest-api.service';
|
||||
import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service';
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
|
||||
const restApiCoreResolvers = [
|
||||
RestApiDeleteOneHandler,
|
||||
RestApiCreateOneHandler,
|
||||
RestApiUpdateOneHandler,
|
||||
RestApiGetOneHandler,
|
||||
RestApiGetManyHandler,
|
||||
];
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
CoreQueryBuilderModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
AuthModule,
|
||||
HttpModule,
|
||||
TwentyORMModule,
|
||||
RecordTransformerModule,
|
||||
WorkspacePermissionsCacheModule,
|
||||
],
|
||||
controllers: [RestApiCoreController, RestApiCoreBatchController],
|
||||
providers: [
|
||||
RestApiService,
|
||||
RestApiCoreService,
|
||||
RestApiCoreServiceV2,
|
||||
ApiEventEmitterService,
|
||||
...coreQueryBuilderFactories,
|
||||
...restApiCoreResolvers,
|
||||
],
|
||||
})
|
||||
export class RestApiCoreModule {}
|
||||
@ -15,36 +15,12 @@ export class RestApiCoreService {
|
||||
private readonly restApiService: RestApiService,
|
||||
) {}
|
||||
|
||||
async get(request: Request) {
|
||||
const data = await this.coreQueryBuilderFactory.get(request);
|
||||
|
||||
return await this.restApiService.call(GraphqlApiType.CORE, request, data);
|
||||
}
|
||||
|
||||
async delete(request: Request) {
|
||||
const data = await this.coreQueryBuilderFactory.delete(request);
|
||||
|
||||
return await this.restApiService.call(GraphqlApiType.CORE, request, data);
|
||||
}
|
||||
|
||||
async createOne(request: Request) {
|
||||
const data = await this.coreQueryBuilderFactory.createOne(request);
|
||||
|
||||
return await this.restApiService.call(GraphqlApiType.CORE, request, data);
|
||||
}
|
||||
|
||||
async createMany(request: Request) {
|
||||
const data = await this.coreQueryBuilderFactory.createMany(request);
|
||||
|
||||
return await this.restApiService.call(GraphqlApiType.CORE, request, data);
|
||||
}
|
||||
|
||||
async update(request: Request) {
|
||||
const data = await this.coreQueryBuilderFactory.update(request);
|
||||
|
||||
return await this.restApiService.call(GraphqlApiType.CORE, request, data);
|
||||
}
|
||||
|
||||
async findDuplicates(request: Request) {
|
||||
const data = await this.coreQueryBuilderFactory.findDuplicates(request);
|
||||
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { ObjectRecordOrderBy } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
export type QueryVariables = {
|
||||
id?: string;
|
||||
ids?: string[];
|
||||
data?: object | null;
|
||||
filter?: object;
|
||||
orderBy?: object;
|
||||
orderBy?: ObjectRecordOrderBy;
|
||||
last?: number;
|
||||
first?: number;
|
||||
startingAfter?: string;
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
|
||||
export const MAX_DEPTH = 2;
|
||||
|
||||
export type Depth = 0 | 1 | 2;
|
||||
|
||||
const ALLOWED_DEPTH_VALUES: Depth[] = [0, 1, MAX_DEPTH];
|
||||
|
||||
@Injectable()
|
||||
export class DepthInputFactory {
|
||||
create(request: Request): Depth {
|
||||
if (!request.query.depth) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const depth = +request.query.depth as Depth;
|
||||
|
||||
if (isNaN(depth) || !ALLOWED_DEPTH_VALUES.includes(depth)) {
|
||||
throw new BadRequestException(
|
||||
`'depth=${
|
||||
request.query.depth
|
||||
}' parameter invalid. Allowed values are ${ALLOWED_DEPTH_VALUES.join(
|
||||
', ',
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return depth;
|
||||
}
|
||||
}
|
||||
@ -3,11 +3,13 @@ import { EndingBeforeInputFactory } from 'src/engine/api/rest/input-factories/en
|
||||
import { LimitInputFactory } from 'src/engine/api/rest/input-factories/limit-input.factory';
|
||||
import { OrderByInputFactory } from 'src/engine/api/rest/input-factories/order-by-input.factory';
|
||||
import { FilterInputFactory } from 'src/engine/api/rest/input-factories/filter-input.factory';
|
||||
import { DepthInputFactory } from 'src/engine/api/rest/input-factories/depth-input.factory';
|
||||
|
||||
export const inputFactories = [
|
||||
StartingAfterInputFactory,
|
||||
DepthInputFactory,
|
||||
EndingBeforeInputFactory,
|
||||
FilterInputFactory,
|
||||
LimitInputFactory,
|
||||
OrderByInputFactory,
|
||||
FilterInputFactory,
|
||||
StartingAfterInputFactory,
|
||||
];
|
||||
|
||||
@ -1,51 +1,23 @@
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service';
|
||||
import { RestApiCoreBatchController } from 'src/engine/api/rest/core/controllers/rest-api-core-batch.controller';
|
||||
import { RestApiCoreController } from 'src/engine/api/rest/core/controllers/rest-api-core.controller';
|
||||
import { CoreQueryBuilderModule } from 'src/engine/api/rest/core/query-builder/core-query-builder.module';
|
||||
import { RestApiCoreServiceV2 } from 'src/engine/api/rest/core/rest-api-core-v2.service';
|
||||
import { RestApiCoreService } from 'src/engine/api/rest/core/rest-api-core.service';
|
||||
import { EndingBeforeInputFactory } from 'src/engine/api/rest/input-factories/ending-before-input.factory';
|
||||
import { LimitInputFactory } from 'src/engine/api/rest/input-factories/limit-input.factory';
|
||||
import { StartingAfterInputFactory } from 'src/engine/api/rest/input-factories/starting-after-input.factory';
|
||||
import { MetadataQueryBuilderModule } from 'src/engine/api/rest/metadata/query-builder/metadata-query-builder.module';
|
||||
import { RestApiMetadataController } from 'src/engine/api/rest/metadata/rest-api-metadata.controller';
|
||||
import { RestApiMetadataService } from 'src/engine/api/rest/metadata/rest-api-metadata.service';
|
||||
import { RestApiService } from 'src/engine/api/rest/rest-api.service';
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module';
|
||||
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
import { RestApiCoreModule } from 'src/engine/api/rest/core/rest-api-core.module';
|
||||
import { RestApiService } from 'src/engine/api/rest/rest-api.service';
|
||||
import { RestApiMetadataController } from 'src/engine/api/rest/metadata/rest-api-metadata.controller';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
CoreQueryBuilderModule,
|
||||
MetadataQueryBuilderModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
AuthModule,
|
||||
HttpModule,
|
||||
TwentyORMModule,
|
||||
RecordTransformerModule,
|
||||
WorkspacePermissionsCacheModule,
|
||||
RestApiCoreModule,
|
||||
],
|
||||
controllers: [
|
||||
RestApiMetadataController,
|
||||
RestApiCoreBatchController,
|
||||
RestApiCoreController,
|
||||
],
|
||||
providers: [
|
||||
RestApiMetadataService,
|
||||
RestApiCoreService,
|
||||
RestApiCoreServiceV2,
|
||||
RestApiService,
|
||||
StartingAfterInputFactory,
|
||||
EndingBeforeInputFactory,
|
||||
LimitInputFactory,
|
||||
ApiEventEmitterService,
|
||||
],
|
||||
exports: [RestApiMetadataService],
|
||||
controllers: [RestApiMetadataController],
|
||||
providers: [RestApiService, RestApiMetadataService],
|
||||
})
|
||||
export class RestApiModule {}
|
||||
|
||||
@ -15,15 +15,15 @@ export class CacheStorageService {
|
||||
) {}
|
||||
|
||||
async get<T>(key: string): Promise<T | undefined> {
|
||||
return this.cache.get(`${this.namespace}:${key}`);
|
||||
return this.cache.get(this.getKey(key));
|
||||
}
|
||||
|
||||
async set<T>(key: string, value: T, ttl?: Milliseconds) {
|
||||
return this.cache.set(`${this.namespace}:${key}`, value, ttl);
|
||||
return this.cache.set(this.getKey(key), value, ttl);
|
||||
}
|
||||
|
||||
async del(key: string) {
|
||||
return this.cache.del(`${this.namespace}:${key}`);
|
||||
return this.cache.del(this.getKey(key));
|
||||
}
|
||||
|
||||
async setAdd(key: string, value: string[], ttl?: Milliseconds) {
|
||||
@ -33,13 +33,13 @@ export class CacheStorageService {
|
||||
|
||||
if (this.isRedisCache()) {
|
||||
await (this.cache as RedisCache).store.client.sAdd(
|
||||
`${this.namespace}:${key}`,
|
||||
this.getKey(key),
|
||||
value,
|
||||
);
|
||||
|
||||
if (ttl) {
|
||||
await (this.cache as RedisCache).store.client.expire(
|
||||
`${this.namespace}:${key}`,
|
||||
this.getKey(key),
|
||||
ttl / 1000,
|
||||
);
|
||||
}
|
||||
@ -65,7 +65,7 @@ export class CacheStorageService {
|
||||
async setPop(key: string, size = 1) {
|
||||
if (this.isRedisCache()) {
|
||||
return (this.cache as RedisCache).store.client.sPop(
|
||||
`${this.namespace}:${key}`,
|
||||
this.getKey(key),
|
||||
size,
|
||||
);
|
||||
}
|
||||
@ -84,7 +84,7 @@ export class CacheStorageService {
|
||||
async getSetLength(key: string) {
|
||||
if (this.isRedisCache()) {
|
||||
return await (this.cache as RedisCache).store.client.sCard(
|
||||
`${this.namespace}:${key}`,
|
||||
this.getKey(key),
|
||||
);
|
||||
}
|
||||
|
||||
@ -125,4 +125,14 @@ export class CacheStorageService {
|
||||
private isRedisCache() {
|
||||
return (this.cache.store as any)?.name === 'redis';
|
||||
}
|
||||
|
||||
private getKey(key: string) {
|
||||
const formattedKey = `${this.namespace}:${key}`;
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return `integration-tests:${formattedKey}`;
|
||||
}
|
||||
|
||||
return formattedKey;
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,8 +24,8 @@ type CacheResult<T, U> = {
|
||||
data: U;
|
||||
};
|
||||
|
||||
const USER_WORKSPACE_ROLE_MAP = 'User workspace role map';
|
||||
const ROLES_PERMISSIONS = 'Roles permissions';
|
||||
export const USER_WORKSPACE_ROLE_MAP = 'User workspace role map';
|
||||
export const ROLES_PERMISSIONS = 'Roles permissions';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspacePermissionsCacheService {
|
||||
|
||||
@ -12,7 +12,10 @@ import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-
|
||||
import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
import { WorkspacePermissionsCacheStorageService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import {
|
||||
ROLES_PERMISSIONS,
|
||||
WorkspacePermissionsCacheService,
|
||||
} from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
|
||||
import {
|
||||
TwentyORMException,
|
||||
@ -229,7 +232,7 @@ export class WorkspaceDatasourceFactory {
|
||||
workspaceId,
|
||||
ignoreLock: true,
|
||||
}),
|
||||
cachedEntityName: 'Roles permissions',
|
||||
cachedEntityName: ROLES_PERMISSIONS,
|
||||
exceptionCode: TwentyORMExceptionCode.ROLES_PERMISSIONS_VERSION_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
import { PERSON_2_ID } from 'test/integration/constants/mock-person-ids.constants';
|
||||
|
||||
export const INITIAL_PERSON_DATA = {
|
||||
id: PERSON_2_ID,
|
||||
name: {
|
||||
firstName: 'Testing',
|
||||
lastName: 'User',
|
||||
},
|
||||
emails: {
|
||||
primaryEmail: 'test8@user.com',
|
||||
additionalEmails: ['user8@example.com'],
|
||||
},
|
||||
city: 'New York',
|
||||
jobTitle: 'Manager',
|
||||
companyId: '20202020-0713-40a5-8216-82802401d33e',
|
||||
};
|
||||
@ -1,4 +0,0 @@
|
||||
export const PERSON_1_ID = '777a8457-eb2d-40ac-a707-551b615b6987';
|
||||
export const PERSON_2_ID = '777a8457-eb2d-40ac-a707-551b615b6988';
|
||||
export const PERSON_3_ID = '777a8457-eb2d-40ac-a707-551b615b6989';
|
||||
export const NOT_EXISTING_PERSON_ID = '777a8457-eb2d-40ac-a707-551b615b6990';
|
||||
@ -0,0 +1 @@
|
||||
export const TEST_COMPANY_1_ID = '525c282e-030a-4a3e-90a0-d8aad0d33a93';
|
||||
@ -0,0 +1,6 @@
|
||||
export const TEST_PERSON_1_ID = '777a8457-eb2d-40ac-a707-551b615b6980';
|
||||
export const TEST_PERSON_2_ID = '777a8457-eb2d-40ac-a707-551b615b6981';
|
||||
export const TEST_PERSON_3_ID = '777a8457-eb2d-40ac-a707-551b615b6982';
|
||||
export const TEST_PERSON_4_ID = '777a8457-eb2d-40ac-a707-551b615b6983';
|
||||
export const NOT_EXISTING_TEST_PERSON_ID =
|
||||
'777a8457-eb2d-40ac-a707-551b615b6990';
|
||||
@ -0,0 +1 @@
|
||||
export const TEST_PRIMARY_LINK_URL = 'http://test/';
|
||||
@ -1,8 +1,8 @@
|
||||
import {
|
||||
PERSON_1_ID,
|
||||
PERSON_2_ID,
|
||||
PERSON_3_ID,
|
||||
} from 'test/integration/constants/mock-person-ids.constants';
|
||||
TEST_PERSON_1_ID,
|
||||
TEST_PERSON_2_ID,
|
||||
TEST_PERSON_3_ID,
|
||||
} from 'test/integration/constants/test-person-ids.constants';
|
||||
import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants';
|
||||
import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util';
|
||||
import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util';
|
||||
@ -16,22 +16,27 @@ import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graph
|
||||
import { updateManyOperationFactory } from 'test/integration/graphql/utils/update-many-operation-factory.util';
|
||||
import { updateOneOperationFactory } from 'test/integration/graphql/utils/update-one-operation-factory.util';
|
||||
import { generateRecordName } from 'test/integration/utils/generate-record-name';
|
||||
import { deleteAllRecords } from 'test/integration/utils/delete-all-records';
|
||||
|
||||
describe('people resolvers (integration)', () => {
|
||||
beforeAll(async () => {
|
||||
await deleteAllRecords('person');
|
||||
});
|
||||
|
||||
it('1. should create and return people', async () => {
|
||||
const personCity1 = generateRecordName(PERSON_1_ID);
|
||||
const personCity2 = generateRecordName(PERSON_2_ID);
|
||||
const personCity1 = generateRecordName(TEST_PERSON_1_ID);
|
||||
const personCity2 = generateRecordName(TEST_PERSON_2_ID);
|
||||
const graphqlOperation = createManyOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
data: [
|
||||
{
|
||||
id: PERSON_1_ID,
|
||||
id: TEST_PERSON_1_ID,
|
||||
city: personCity1,
|
||||
},
|
||||
{
|
||||
id: PERSON_2_ID,
|
||||
id: TEST_PERSON_2_ID,
|
||||
city: personCity2,
|
||||
},
|
||||
],
|
||||
@ -57,13 +62,13 @@ describe('people resolvers (integration)', () => {
|
||||
});
|
||||
|
||||
it('1b. should create and return one person', async () => {
|
||||
const personCity3 = generateRecordName(PERSON_3_ID);
|
||||
const personCity3 = generateRecordName(TEST_PERSON_3_ID);
|
||||
|
||||
const graphqlOperation = createOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
data: {
|
||||
id: PERSON_3_ID,
|
||||
id: TEST_PERSON_3_ID,
|
||||
city: personCity3,
|
||||
},
|
||||
});
|
||||
@ -121,7 +126,7 @@ describe('people resolvers (integration)', () => {
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
filter: {
|
||||
id: {
|
||||
eq: PERSON_3_ID,
|
||||
eq: TEST_PERSON_3_ID,
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -152,7 +157,7 @@ describe('people resolvers (integration)', () => {
|
||||
},
|
||||
filter: {
|
||||
id: {
|
||||
in: [PERSON_1_ID, PERSON_2_ID],
|
||||
in: [TEST_PERSON_1_ID, TEST_PERSON_2_ID],
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -175,7 +180,7 @@ describe('people resolvers (integration)', () => {
|
||||
data: {
|
||||
city: 'New City',
|
||||
},
|
||||
recordId: PERSON_3_ID,
|
||||
recordId: TEST_PERSON_3_ID,
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
@ -225,7 +230,7 @@ describe('people resolvers (integration)', () => {
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
filter: {
|
||||
id: {
|
||||
in: [PERSON_1_ID, PERSON_2_ID],
|
||||
in: [TEST_PERSON_1_ID, TEST_PERSON_2_ID],
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -245,7 +250,7 @@ describe('people resolvers (integration)', () => {
|
||||
const graphqlOperation = deleteOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
recordId: PERSON_3_ID,
|
||||
recordId: TEST_PERSON_3_ID,
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
@ -260,7 +265,7 @@ describe('people resolvers (integration)', () => {
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
filter: {
|
||||
id: {
|
||||
in: [PERSON_1_ID, PERSON_2_ID],
|
||||
in: [TEST_PERSON_1_ID, TEST_PERSON_2_ID],
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -276,7 +281,7 @@ describe('people resolvers (integration)', () => {
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
filter: {
|
||||
id: {
|
||||
eq: PERSON_3_ID,
|
||||
eq: TEST_PERSON_3_ID,
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -293,7 +298,7 @@ describe('people resolvers (integration)', () => {
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
filter: {
|
||||
id: {
|
||||
in: [PERSON_1_ID, PERSON_2_ID],
|
||||
in: [TEST_PERSON_1_ID, TEST_PERSON_2_ID],
|
||||
},
|
||||
not: {
|
||||
deletedAt: {
|
||||
@ -314,7 +319,7 @@ describe('people resolvers (integration)', () => {
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
filter: {
|
||||
id: {
|
||||
eq: PERSON_3_ID,
|
||||
eq: TEST_PERSON_3_ID,
|
||||
},
|
||||
not: {
|
||||
deletedAt: {
|
||||
@ -326,7 +331,7 @@ describe('people resolvers (integration)', () => {
|
||||
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
|
||||
expect(response.body.data.person.id).toEqual(PERSON_3_ID);
|
||||
expect(response.body.data.person.id).toEqual(TEST_PERSON_3_ID);
|
||||
});
|
||||
|
||||
it('8. should destroy many people', async () => {
|
||||
@ -336,7 +341,7 @@ describe('people resolvers (integration)', () => {
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
filter: {
|
||||
id: {
|
||||
in: [PERSON_1_ID, PERSON_2_ID],
|
||||
in: [TEST_PERSON_1_ID, TEST_PERSON_2_ID],
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -350,7 +355,7 @@ describe('people resolvers (integration)', () => {
|
||||
const graphqlOperation = destroyOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
recordId: PERSON_3_ID,
|
||||
recordId: TEST_PERSON_3_ID,
|
||||
});
|
||||
|
||||
const destroyPeopleResponse = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
@ -365,7 +370,7 @@ describe('people resolvers (integration)', () => {
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
filter: {
|
||||
id: {
|
||||
in: [PERSON_1_ID, PERSON_2_ID],
|
||||
in: [TEST_PERSON_1_ID, TEST_PERSON_2_ID],
|
||||
},
|
||||
not: {
|
||||
deletedAt: {
|
||||
@ -386,7 +391,7 @@ describe('people resolvers (integration)', () => {
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
filter: {
|
||||
id: {
|
||||
eq: PERSON_3_ID,
|
||||
eq: TEST_PERSON_3_ID,
|
||||
},
|
||||
not: {
|
||||
deletedAt: {
|
||||
|
||||
@ -8,7 +8,7 @@ const ORIGIN = new URL(SERVER_URL);
|
||||
|
||||
ORIGIN.hostname =
|
||||
process.env.IS_MULTIWORKSPACE_ENABLED === 'true'
|
||||
? `acme.${ORIGIN.hostname}`
|
||||
? `apple.${ORIGIN.hostname}`
|
||||
: ORIGIN.hostname;
|
||||
|
||||
const auth = {
|
||||
|
||||
@ -14,8 +14,6 @@ import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.
|
||||
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
|
||||
describe('datamodel permissions', () => {
|
||||
beforeAll(async () => {});
|
||||
|
||||
describe('fieldMetadata', () => {
|
||||
let listingObjectId = '';
|
||||
let testFieldId = '';
|
||||
|
||||
@ -1,21 +1,31 @@
|
||||
import { PERSON_1_ID } from 'test/integration/constants/mock-person-ids.constants';
|
||||
import { TEST_PERSON_1_ID } from 'test/integration/constants/test-person-ids.constants';
|
||||
import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util';
|
||||
import { deleteAllRecords } from 'test/integration/utils/delete-all-records';
|
||||
import { generateRecordName } from 'test/integration/utils/generate-record-name';
|
||||
import { TEST_COMPANY_1_ID } from 'test/integration/constants/test-company-ids.constants';
|
||||
import { TEST_PRIMARY_LINK_URL } from 'test/integration/constants/test-primary-link-url.constant';
|
||||
|
||||
describe('Core REST API Create One endpoint', () => {
|
||||
beforeAll(
|
||||
async () =>
|
||||
await makeRestAPIRequest({
|
||||
method: 'delete',
|
||||
path: `/people/${PERSON_1_ID}`,
|
||||
}),
|
||||
);
|
||||
beforeEach(async () => {
|
||||
await deleteAllRecords('person');
|
||||
await makeRestAPIRequest({
|
||||
method: 'post',
|
||||
path: '/companies',
|
||||
body: {
|
||||
id: TEST_COMPANY_1_ID,
|
||||
domainName: {
|
||||
primaryLinkUrl: TEST_PRIMARY_LINK_URL,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a new person', async () => {
|
||||
const personCity = generateRecordName(PERSON_1_ID);
|
||||
const personCity = generateRecordName(TEST_PERSON_1_ID);
|
||||
const requestBody = {
|
||||
id: PERSON_1_ID,
|
||||
id: TEST_PERSON_1_ID,
|
||||
city: personCity,
|
||||
companyId: TEST_COMPANY_1_ID,
|
||||
};
|
||||
|
||||
await makeRestAPIRequest({
|
||||
@ -27,18 +37,97 @@ describe('Core REST API Create One endpoint', () => {
|
||||
.expect((res) => {
|
||||
const createdPerson = res.body.data.createPerson;
|
||||
|
||||
expect(createdPerson.id).toBe(PERSON_1_ID);
|
||||
expect(createdPerson.id).toBe(TEST_PERSON_1_ID);
|
||||
expect(createdPerson.city).toBe(personCity);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a BadRequestException when trying to create a person with an existing ID', async () => {
|
||||
const personCity = generateRecordName(PERSON_1_ID);
|
||||
it('should support depth 0 parameter', async () => {
|
||||
const personCity = generateRecordName(TEST_PERSON_1_ID);
|
||||
const requestBody = {
|
||||
id: PERSON_1_ID,
|
||||
id: TEST_PERSON_1_ID,
|
||||
city: personCity,
|
||||
companyId: TEST_COMPANY_1_ID,
|
||||
};
|
||||
|
||||
await makeRestAPIRequest({
|
||||
method: 'post',
|
||||
path: `/people?depth=0`,
|
||||
body: requestBody,
|
||||
})
|
||||
.expect(201)
|
||||
.expect((res) => {
|
||||
const createdPerson = res.body.data.createPerson;
|
||||
|
||||
expect(createdPerson.companyId).toBeDefined();
|
||||
expect(createdPerson.company).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support depth 1 parameter', async () => {
|
||||
const personCity = generateRecordName(TEST_PERSON_1_ID);
|
||||
const requestBody = {
|
||||
id: TEST_PERSON_1_ID,
|
||||
city: personCity,
|
||||
companyId: TEST_COMPANY_1_ID,
|
||||
};
|
||||
|
||||
await makeRestAPIRequest({
|
||||
method: 'post',
|
||||
path: `/people?depth=1`,
|
||||
body: requestBody,
|
||||
})
|
||||
.expect(201)
|
||||
.expect((res) => {
|
||||
const createdPerson = res.body.data.createPerson;
|
||||
|
||||
expect(createdPerson.company).toBeDefined();
|
||||
expect(createdPerson.company.domainName.primaryLinkUrl).toBe(
|
||||
TEST_PRIMARY_LINK_URL,
|
||||
);
|
||||
expect(createdPerson.company.people).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support depth 2 parameter', async () => {
|
||||
const personCity = generateRecordName(TEST_PERSON_1_ID);
|
||||
const requestBody = {
|
||||
id: TEST_PERSON_1_ID,
|
||||
city: personCity,
|
||||
companyId: TEST_COMPANY_1_ID,
|
||||
};
|
||||
|
||||
await makeRestAPIRequest({
|
||||
method: 'post',
|
||||
path: `/people?depth=2`,
|
||||
body: requestBody,
|
||||
})
|
||||
.expect(201)
|
||||
.expect((res) => {
|
||||
const createdPerson = res.body.data.createPerson;
|
||||
|
||||
expect(createdPerson.company.people).toBeDefined();
|
||||
const depth2Person = createdPerson.company.people.find(
|
||||
(p) => p.id === createdPerson.id,
|
||||
);
|
||||
|
||||
expect(depth2Person).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a BadRequestException when trying to create a person with an existing ID', async () => {
|
||||
const personCity = generateRecordName(TEST_PERSON_1_ID);
|
||||
const requestBody = {
|
||||
id: TEST_PERSON_1_ID,
|
||||
city: personCity,
|
||||
};
|
||||
|
||||
await makeRestAPIRequest({
|
||||
method: 'post',
|
||||
path: `/people`,
|
||||
body: requestBody,
|
||||
});
|
||||
|
||||
await makeRestAPIRequest({
|
||||
method: 'post',
|
||||
path: `/people`,
|
||||
|
||||
@ -1,34 +1,59 @@
|
||||
import {
|
||||
NOT_EXISTING_PERSON_ID,
|
||||
PERSON_1_ID,
|
||||
} from 'test/integration/constants/mock-person-ids.constants';
|
||||
NOT_EXISTING_TEST_PERSON_ID,
|
||||
TEST_PERSON_1_ID,
|
||||
} from 'test/integration/constants/test-person-ids.constants';
|
||||
import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util';
|
||||
import { deleteAllRecords } from 'test/integration/utils/delete-all-records';
|
||||
|
||||
describe('Core REST API Delete One endpoint', () => {
|
||||
beforeAll(
|
||||
async () =>
|
||||
await makeRestAPIRequest({
|
||||
method: 'post',
|
||||
path: `/people`,
|
||||
body: {
|
||||
id: PERSON_1_ID,
|
||||
},
|
||||
}),
|
||||
);
|
||||
beforeAll(async () => {
|
||||
await deleteAllRecords('person');
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await makeRestAPIRequest({
|
||||
method: 'post',
|
||||
path: `/people`,
|
||||
body: {
|
||||
id: TEST_PERSON_1_ID,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete one person', async () => {
|
||||
await makeRestAPIRequest({
|
||||
method: 'delete',
|
||||
path: `/people/${PERSON_1_ID}`,
|
||||
path: `/people/${TEST_PERSON_1_ID}`,
|
||||
})
|
||||
.expect(200)
|
||||
.expect((res) => expect(res.body.data.deletePerson.id).toBe(PERSON_1_ID));
|
||||
.expect((res) =>
|
||||
expect(res.body.data.deletePerson).toEqual({ id: TEST_PERSON_1_ID }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should delete one person with favorite', async () => {
|
||||
await makeRestAPIRequest({
|
||||
method: 'post',
|
||||
path: `/favorites`,
|
||||
body: {
|
||||
personId: TEST_PERSON_1_ID,
|
||||
},
|
||||
});
|
||||
|
||||
await makeRestAPIRequest({
|
||||
method: 'delete',
|
||||
path: `/people/${TEST_PERSON_1_ID}`,
|
||||
})
|
||||
.expect(200)
|
||||
.expect((res) =>
|
||||
expect(res.body.data.deletePerson).toEqual({ id: TEST_PERSON_1_ID }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return a EntityNotFoundError when trying to delete a non-existing person', async () => {
|
||||
await makeRestAPIRequest({
|
||||
method: 'delete',
|
||||
path: `/people/${NOT_EXISTING_PERSON_ID}`,
|
||||
path: `/people/${NOT_EXISTING_TEST_PERSON_ID}`,
|
||||
})
|
||||
.expect(400)
|
||||
.expect((res) => {
|
||||
|
||||
@ -0,0 +1,289 @@
|
||||
import {
|
||||
TEST_PERSON_1_ID,
|
||||
TEST_PERSON_2_ID,
|
||||
TEST_PERSON_3_ID,
|
||||
TEST_PERSON_4_ID,
|
||||
} from 'test/integration/constants/test-person-ids.constants';
|
||||
import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util';
|
||||
import { generateRecordName } from 'test/integration/utils/generate-record-name';
|
||||
import { TEST_COMPANY_1_ID } from 'test/integration/constants/test-company-ids.constants';
|
||||
import { deleteAllRecords } from 'test/integration/utils/delete-all-records';
|
||||
import { TEST_PRIMARY_LINK_URL } from 'test/integration/constants/test-primary-link-url.constant';
|
||||
|
||||
describe('Core REST API Find Many endpoint', () => {
|
||||
const testPersonIds = [
|
||||
TEST_PERSON_1_ID,
|
||||
TEST_PERSON_2_ID,
|
||||
TEST_PERSON_3_ID,
|
||||
TEST_PERSON_4_ID,
|
||||
];
|
||||
const testPersonCities: Record<string, string> = {};
|
||||
|
||||
beforeAll(async () => {
|
||||
await deleteAllRecords('person');
|
||||
|
||||
await makeRestAPIRequest({
|
||||
method: 'post',
|
||||
path: '/companies',
|
||||
body: {
|
||||
id: TEST_COMPANY_1_ID,
|
||||
domainName: {
|
||||
primaryLinkUrl: TEST_PRIMARY_LINK_URL,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let index = 0;
|
||||
|
||||
for (const personId of testPersonIds) {
|
||||
const city = generateRecordName(personId);
|
||||
|
||||
testPersonCities[personId] = city;
|
||||
|
||||
await makeRestAPIRequest({
|
||||
method: 'post',
|
||||
path: '/people',
|
||||
body: {
|
||||
id: personId,
|
||||
city: city,
|
||||
position: index,
|
||||
companyId: TEST_COMPANY_1_ID,
|
||||
},
|
||||
});
|
||||
index++;
|
||||
}
|
||||
});
|
||||
|
||||
it('should retrieve all people with pagination metadata', async () => {
|
||||
const response = await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
path: '/people',
|
||||
}).expect(200);
|
||||
|
||||
const people = response.body.data.people;
|
||||
const pageInfo = response.body.pageInfo;
|
||||
const totalCount = response.body.totalCount;
|
||||
|
||||
expect(people).not.toBeNull();
|
||||
expect(Array.isArray(people)).toBe(true);
|
||||
expect(people.length).toBeGreaterThanOrEqual(testPersonIds.length);
|
||||
|
||||
// Check that our test people are included in the results
|
||||
for (const personId of testPersonIds) {
|
||||
const person = people.find((p) => p.id === personId);
|
||||
|
||||
expect(person).toBeDefined();
|
||||
expect(person.city).toBe(testPersonCities[personId]);
|
||||
}
|
||||
|
||||
// Check pagination metadata
|
||||
expect(pageInfo).toBeDefined();
|
||||
expect(pageInfo.startCursor).toBeDefined();
|
||||
expect(pageInfo.endCursor).toBeDefined();
|
||||
expect(typeof totalCount).toBe('number');
|
||||
expect(totalCount).toEqual(testPersonIds.length);
|
||||
expect(response.body.pageInfo.hasNextPage).toBe(false);
|
||||
});
|
||||
|
||||
it('should limit results based on the limit parameter', async () => {
|
||||
const limit = testPersonIds.length - 1;
|
||||
const response = await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
path: `/people?limit=${limit}`,
|
||||
}).expect(200);
|
||||
|
||||
const people = response.body.data.people;
|
||||
|
||||
expect(people).not.toBeNull();
|
||||
expect(Array.isArray(people)).toBe(true);
|
||||
expect(people.length).toEqual(limit);
|
||||
expect(response.body.totalCount).toEqual(testPersonIds.length);
|
||||
expect(response.body.pageInfo.hasNextPage).toBe(true);
|
||||
});
|
||||
|
||||
it('should return filtered totalCount', async () => {
|
||||
const response = await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
path: `/people?filter=position[lte]:1`,
|
||||
}).expect(200);
|
||||
|
||||
const people = response.body.data.people;
|
||||
|
||||
expect(people).not.toBeNull();
|
||||
expect(Array.isArray(people)).toBe(true);
|
||||
expect(people.length).toEqual(2);
|
||||
expect(response.body.totalCount).toEqual(2);
|
||||
expect(response.body.pageInfo.hasNextPage).toBe(false);
|
||||
});
|
||||
|
||||
it('should filter results based on filter parameters', async () => {
|
||||
const response = await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
path: '/people?filter=position[lt]:2',
|
||||
}).expect(200);
|
||||
|
||||
const filteredPeople = response.body.data.people;
|
||||
|
||||
expect(filteredPeople).toBeDefined();
|
||||
expect(Array.isArray(filteredPeople)).toBe(true);
|
||||
expect(filteredPeople.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should support cursor-based pagination with starting_after', async () => {
|
||||
const initialResponse = await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
path: '/people?limit=2',
|
||||
}).expect(200);
|
||||
|
||||
const people = initialResponse.body.data.people;
|
||||
const startCursor = initialResponse.body.pageInfo.startCursor;
|
||||
|
||||
expect(people).toBeDefined();
|
||||
expect(people.length).toBe(2);
|
||||
expect(startCursor).toBeDefined();
|
||||
|
||||
const nextPageResponse = await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
path: `/people?starting_after=${startCursor}&limit=1`,
|
||||
}).expect(200);
|
||||
|
||||
const nextPagePeople = nextPageResponse.body.data.people;
|
||||
|
||||
expect(nextPagePeople).toBeDefined();
|
||||
expect(nextPagePeople.length).toBe(1);
|
||||
expect(nextPagePeople[0].id).toBe(people[1].id);
|
||||
});
|
||||
|
||||
it('should support cursor-based pagination with ending_before', async () => {
|
||||
const initialResponse = await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
path: '/people?limit=4',
|
||||
}).expect(200);
|
||||
|
||||
const people = initialResponse.body.data.people;
|
||||
const endCursor = initialResponse.body.pageInfo.endCursor;
|
||||
|
||||
expect(people).toBeDefined();
|
||||
expect(people.length).toBe(4);
|
||||
expect(endCursor).toBeDefined();
|
||||
|
||||
const nextPageResponse = await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
path: `/people?ending_before=${endCursor}&limit=2`,
|
||||
}).expect(200);
|
||||
|
||||
const nextPagePeople = nextPageResponse.body.data.people;
|
||||
|
||||
expect(nextPagePeople).toBeDefined();
|
||||
expect(nextPagePeople.length).toBe(2);
|
||||
expect(nextPagePeople[0].id).toBe(people[1].id);
|
||||
expect(nextPagePeople[1].id).toBe(people[2].id);
|
||||
});
|
||||
|
||||
it('should support ordering Asc of results', async () => {
|
||||
const ascResponse = await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
path: '/people?order_by=position[AscNullsLast]',
|
||||
}).expect(200);
|
||||
|
||||
const ascPeople = ascResponse.body.data.people;
|
||||
|
||||
expect(ascPeople).toEqual(
|
||||
[...ascPeople].sort((a, b) => a.position - b.position),
|
||||
);
|
||||
});
|
||||
|
||||
it('should support ordering Desc of results', async () => {
|
||||
const descResponse = await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
path: '/people?order_by=position[DescNullsLast]',
|
||||
}).expect(200);
|
||||
|
||||
const descPeople = descResponse.body.data.people;
|
||||
|
||||
expect(descPeople).toEqual(
|
||||
[...descPeople].sort((a, b) => -(a.position - b.position)),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle invalid cursor gracefully', async () => {
|
||||
await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
path: '/people?starting_after=invalid-cursor',
|
||||
})
|
||||
.expect(400)
|
||||
.expect((res) => {
|
||||
expect(res.body.error).toBe('BadRequestException');
|
||||
expect(res.body.messages[0]).toContain('Invalid cursor');
|
||||
});
|
||||
});
|
||||
|
||||
it('should combine filtering, ordering, and pagination', async () => {
|
||||
const response = await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
path: '/people?filter=position[gt]:0&order_by=city[AscNullsFirst]&limit=2',
|
||||
}).expect(200);
|
||||
|
||||
const people = response.body.data.people;
|
||||
|
||||
const pageInfo = response.body.pageInfo;
|
||||
|
||||
expect(people).toBeDefined();
|
||||
expect(people.length).toBeLessThanOrEqual(2);
|
||||
expect(pageInfo).toBeDefined();
|
||||
|
||||
expect(people).toEqual([...people].sort((a, b) => a.city - b.city));
|
||||
});
|
||||
|
||||
it('should support depth 0 parameter', async () => {
|
||||
const response = await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
path: '/people?depth=0',
|
||||
}).expect(200);
|
||||
|
||||
const people = response.body.data.people;
|
||||
|
||||
expect(people).toBeDefined();
|
||||
|
||||
const person = people[0];
|
||||
|
||||
expect(person).toBeDefined();
|
||||
expect(person.companyId).toBeDefined();
|
||||
expect(person.company).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('should support depth 1 parameter', async () => {
|
||||
const response = await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
path: '/people?depth=1',
|
||||
}).expect(200);
|
||||
|
||||
const people = response.body.data.people;
|
||||
|
||||
const person = people[0];
|
||||
|
||||
expect(person.company).toBeDefined();
|
||||
expect(person.company.domainName.primaryLinkUrl).toBe(
|
||||
TEST_PRIMARY_LINK_URL,
|
||||
);
|
||||
|
||||
expect(person.company.people).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('should support depth 2 parameter', async () => {
|
||||
const response = await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
path: '/people?depth=2',
|
||||
}).expect(200);
|
||||
|
||||
const people = response.body.data.people;
|
||||
|
||||
const person = people[0];
|
||||
|
||||
expect(person.company.people).toBeDefined();
|
||||
|
||||
const depth2Person = person.company.people.find((p) => p.id === person.id);
|
||||
|
||||
expect(depth2Person).toBeDefined();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,119 @@
|
||||
import {
|
||||
NOT_EXISTING_TEST_PERSON_ID,
|
||||
TEST_PERSON_1_ID,
|
||||
} from 'test/integration/constants/test-person-ids.constants';
|
||||
import { TEST_COMPANY_1_ID } from 'test/integration/constants/test-company-ids.constants';
|
||||
import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util';
|
||||
import { generateRecordName } from 'test/integration/utils/generate-record-name';
|
||||
import { deleteAllRecords } from 'test/integration/utils/delete-all-records';
|
||||
import { TEST_PRIMARY_LINK_URL } from 'test/integration/constants/test-primary-link-url.constant';
|
||||
|
||||
describe('Core REST API Find One endpoint', () => {
|
||||
let personCity: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
await deleteAllRecords('person');
|
||||
await deleteAllRecords('company');
|
||||
|
||||
personCity = generateRecordName(TEST_PERSON_1_ID);
|
||||
|
||||
await makeRestAPIRequest({
|
||||
method: 'post',
|
||||
path: '/companies',
|
||||
body: {
|
||||
id: TEST_COMPANY_1_ID,
|
||||
domainName: {
|
||||
primaryLinkUrl: TEST_PRIMARY_LINK_URL,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await makeRestAPIRequest({
|
||||
method: 'post',
|
||||
path: '/people',
|
||||
body: {
|
||||
id: TEST_PERSON_1_ID,
|
||||
city: personCity,
|
||||
companyId: TEST_COMPANY_1_ID,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should retrieve a person by ID', async () => {
|
||||
await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
path: `/people/${TEST_PERSON_1_ID}`,
|
||||
})
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
const person = res.body.data.person;
|
||||
|
||||
expect(person).not.toBeNull();
|
||||
expect(person.id).toBe(TEST_PERSON_1_ID);
|
||||
expect(person.city).toBe(personCity);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 error when trying to retrieve a non-existing person', async () => {
|
||||
await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
path: `/people/${NOT_EXISTING_TEST_PERSON_ID}`,
|
||||
})
|
||||
.expect(400)
|
||||
.expect((res) => {
|
||||
expect(res.body.messages[0]).toContain('Record not found');
|
||||
expect(res.body.error).toBe('BadRequestException');
|
||||
});
|
||||
});
|
||||
|
||||
it('should support depth 0 parameter', async () => {
|
||||
await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
path: `/people/${TEST_PERSON_1_ID}?depth=0`,
|
||||
})
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
const person = res.body.data.person;
|
||||
|
||||
expect(person).toBeDefined();
|
||||
expect(person.companyId).toBeDefined();
|
||||
expect(person.company).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support depth 1 parameter', async () => {
|
||||
await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
path: `/people/${TEST_PERSON_1_ID}?depth=1`,
|
||||
})
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
const person = res.body.data.person;
|
||||
|
||||
expect(person.company).toBeDefined();
|
||||
expect(person.company.domainName.primaryLinkUrl).toBe(
|
||||
TEST_PRIMARY_LINK_URL,
|
||||
);
|
||||
expect(person.company.people).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support depth 2 parameter', async () => {
|
||||
await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
path: `/people/${TEST_PERSON_1_ID}?depth=2`,
|
||||
})
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
const person = res.body.data.person;
|
||||
|
||||
expect(person.company.people).toBeDefined();
|
||||
|
||||
const depth2Person = person.company.people.find(
|
||||
(p) => p.id === person.id,
|
||||
);
|
||||
|
||||
expect(depth2Person).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,48 +1,59 @@
|
||||
import {
|
||||
NOT_EXISTING_PERSON_ID,
|
||||
PERSON_1_ID,
|
||||
} from 'test/integration/constants/mock-person-ids.constants';
|
||||
NOT_EXISTING_TEST_PERSON_ID,
|
||||
TEST_PERSON_1_ID,
|
||||
} from 'test/integration/constants/test-person-ids.constants';
|
||||
import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util';
|
||||
import { generateRecordName } from 'test/integration/utils/generate-record-name';
|
||||
import { deleteAllRecords } from 'test/integration/utils/delete-all-records';
|
||||
import { TEST_COMPANY_1_ID } from 'test/integration/constants/test-company-ids.constants';
|
||||
import { TEST_PRIMARY_LINK_URL } from 'test/integration/constants/test-primary-link-url.constant';
|
||||
|
||||
describe('Core REST API Update One endpoint', () => {
|
||||
const updatedData = {
|
||||
name: {
|
||||
firstName: 'Updated',
|
||||
lastName: 'Person',
|
||||
},
|
||||
emails: {
|
||||
primaryEmail: 'updated@example.com',
|
||||
additionalEmails: ['extra@example.com'],
|
||||
},
|
||||
city: generateRecordName(TEST_PERSON_1_ID),
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
await deleteAllRecords('person');
|
||||
await makeRestAPIRequest({
|
||||
method: 'delete',
|
||||
path: `/people/${PERSON_1_ID}`,
|
||||
method: 'post',
|
||||
path: '/companies',
|
||||
body: {
|
||||
id: TEST_COMPANY_1_ID,
|
||||
domainName: {
|
||||
primaryLinkUrl: TEST_PRIMARY_LINK_URL,
|
||||
},
|
||||
},
|
||||
});
|
||||
await makeRestAPIRequest({
|
||||
method: 'post',
|
||||
path: `/people`,
|
||||
body: {
|
||||
id: PERSON_1_ID,
|
||||
id: TEST_PERSON_1_ID,
|
||||
companyId: TEST_COMPANY_1_ID,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should update an existing person (name, emails, and city)', async () => {
|
||||
const updatedData = {
|
||||
name: {
|
||||
firstName: 'Updated',
|
||||
lastName: 'Person',
|
||||
},
|
||||
emails: {
|
||||
primaryEmail: 'updated@example.com',
|
||||
additionalEmails: ['extra@example.com'],
|
||||
},
|
||||
city: generateRecordName(PERSON_1_ID),
|
||||
};
|
||||
|
||||
await makeRestAPIRequest({
|
||||
method: 'patch',
|
||||
path: `/people/${PERSON_1_ID}`,
|
||||
path: `/people/${TEST_PERSON_1_ID}`,
|
||||
body: updatedData,
|
||||
})
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
const updatedPerson = res.body.data.updatePerson;
|
||||
|
||||
expect(updatedPerson.id).toBe(PERSON_1_ID);
|
||||
expect(updatedPerson.id).toBe(TEST_PERSON_1_ID);
|
||||
expect(updatedPerson.name.firstName).toBe(updatedData.name.firstName);
|
||||
expect(updatedPerson.name.lastName).toBe(updatedData.name.lastName);
|
||||
expect(updatedPerson.emails.primaryEmail).toBe(
|
||||
@ -54,14 +65,67 @@ describe('Core REST API Update One endpoint', () => {
|
||||
expect(updatedPerson.city).toBe(updatedData.city);
|
||||
|
||||
expect(updatedPerson.jobTitle).toBe('');
|
||||
expect(updatedPerson.companyId).toBe(null);
|
||||
expect(updatedPerson.companyId).toBe(TEST_COMPANY_1_ID);
|
||||
});
|
||||
});
|
||||
|
||||
it('should support depth 0 parameter', async () => {
|
||||
await makeRestAPIRequest({
|
||||
method: 'patch',
|
||||
path: `/people/${TEST_PERSON_1_ID}?depth=0`,
|
||||
body: updatedData,
|
||||
})
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
const updatedPerson = res.body.data.updatePerson;
|
||||
|
||||
expect(updatedPerson.companyId).toBeDefined();
|
||||
expect(updatedPerson.company).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support depth 1 parameter', async () => {
|
||||
await makeRestAPIRequest({
|
||||
method: 'patch',
|
||||
path: `/people/${TEST_PERSON_1_ID}?depth=1`,
|
||||
body: updatedData,
|
||||
})
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
const updatedPerson = res.body.data.updatePerson;
|
||||
|
||||
expect(updatedPerson.company).toBeDefined();
|
||||
expect(updatedPerson.company.domainName.primaryLinkUrl).toBe(
|
||||
TEST_PRIMARY_LINK_URL,
|
||||
);
|
||||
expect(updatedPerson.company.people).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support depth 2 parameter', async () => {
|
||||
await makeRestAPIRequest({
|
||||
method: 'patch',
|
||||
path: `/people/${TEST_PERSON_1_ID}?depth=2`,
|
||||
body: updatedData,
|
||||
})
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
const updatedPerson = res.body.data.updatePerson;
|
||||
|
||||
expect(updatedPerson.company.people).toBeDefined();
|
||||
|
||||
const depth2Person = updatedPerson.company.people.find(
|
||||
(p) => p.id === updatedPerson.id,
|
||||
);
|
||||
|
||||
expect(depth2Person).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a EntityNotFoundError when trying to update a non-existing person', async () => {
|
||||
await makeRestAPIRequest({
|
||||
method: 'patch',
|
||||
path: `/people/${NOT_EXISTING_PERSON_ID}`,
|
||||
path: `/people/${NOT_EXISTING_TEST_PERSON_ID}`,
|
||||
})
|
||||
.expect(400)
|
||||
.expect((res) => {
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
const TEST_SCHEMA_NAME = 'workspace_1wgvd1injqtife6y4rvfbu3h5';
|
||||
|
||||
export const deleteAllRecords = async (objectNameSingular: string) => {
|
||||
try {
|
||||
await global.testDataSource.query(
|
||||
`DELETE from "${TEST_SCHEMA_NAME}"."${objectNameSingular}"`,
|
||||
);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
};
|
||||
@ -1,6 +1,8 @@
|
||||
import { JestConfigWithTsJest } from 'ts-jest';
|
||||
import 'tsconfig-paths/register';
|
||||
|
||||
import { rawDataSource } from 'src/database/typeorm/raw/raw.datasource';
|
||||
|
||||
import { createApp } from './create-app';
|
||||
|
||||
export default async (_, projectConfig: JestConfigWithTsJest) => {
|
||||
@ -10,7 +12,10 @@ export default async (_, projectConfig: JestConfigWithTsJest) => {
|
||||
throw new Error('No globals found in project config');
|
||||
}
|
||||
|
||||
await rawDataSource.initialize();
|
||||
|
||||
await app.listen(projectConfig.globals.APP_PORT);
|
||||
|
||||
global.app = app;
|
||||
global.testDataSource = rawDataSource;
|
||||
};
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import 'tsconfig-paths/register';
|
||||
|
||||
export default async () => {
|
||||
global.testDataSource.destroy();
|
||||
global.app.close();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user