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
|
docker run -d --name twenty_redis -p 6379:6379 redis/redis-stack-server:latest
|
||||||
|
|
||||||
clickhouse-on-docker:
|
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.POST,
|
||||||
RequestMethod.PATCH,
|
RequestMethod.PATCH,
|
||||||
RequestMethod.PUT,
|
RequestMethod.PUT,
|
||||||
|
RequestMethod.GET,
|
||||||
];
|
];
|
||||||
|
|
||||||
@Module({
|
@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 { 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 { 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 { 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', () => {
|
describe('WorkspaceSchemaFactory', () => {
|
||||||
let service: WorkspaceSchemaFactory;
|
let service: WorkspaceSchemaFactory;
|
||||||
@ -49,6 +50,10 @@ describe('WorkspaceSchemaFactory', () => {
|
|||||||
provide: FeatureFlagService,
|
provide: FeatureFlagService,
|
||||||
useValue: {},
|
useValue: {},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: TwentyConfigService,
|
||||||
|
useValue: {},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { capitalize } from 'twenty-shared/utils';
|
import { capitalize } from 'twenty-shared/utils';
|
||||||
import { WhereExpressionBuilder } from 'typeorm';
|
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 { 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 {
|
import {
|
||||||
GraphqlQueryRunnerException,
|
GraphqlQueryRunnerException,
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import {
|
|||||||
ObjectRecordOrderBy,
|
ObjectRecordOrderBy,
|
||||||
OrderByDirection,
|
OrderByDirection,
|
||||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
} 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 { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -18,14 +17,9 @@ import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspac
|
|||||||
|
|
||||||
export class GraphqlQueryOrderFieldParser {
|
export class GraphqlQueryOrderFieldParser {
|
||||||
private fieldMetadataMapByName: FieldMetadataMap;
|
private fieldMetadataMapByName: FieldMetadataMap;
|
||||||
private featureFlagsMap: FeatureFlagMap;
|
|
||||||
|
|
||||||
constructor(
|
constructor(fieldMetadataMapByName: FieldMetadataMap) {
|
||||||
fieldMetadataMapByName: FieldMetadataMap,
|
|
||||||
featureFlagsMap: FeatureFlagMap,
|
|
||||||
) {
|
|
||||||
this.fieldMetadataMapByName = fieldMetadataMapByName;
|
this.fieldMetadataMapByName = fieldMetadataMapByName;
|
||||||
this.featureFlagsMap = featureFlagsMap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
parse(
|
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 { 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 { 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 { 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 {
|
export class GraphqlQueryParser {
|
||||||
private fieldMetadataMapByName: FieldMetadataMap;
|
private fieldMetadataMapByName: FieldMetadataMap;
|
||||||
@ -47,7 +51,6 @@ export class GraphqlQueryParser {
|
|||||||
);
|
);
|
||||||
this.orderFieldParser = new GraphqlQueryOrderFieldParser(
|
this.orderFieldParser = new GraphqlQueryOrderFieldParser(
|
||||||
this.fieldMetadataMapByName,
|
this.fieldMetadataMapByName,
|
||||||
featureFlagsMap,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,8 +128,9 @@ export class GraphqlQueryParser {
|
|||||||
)?.fieldsByName;
|
)?.fieldsByName;
|
||||||
|
|
||||||
if (!parentFields) {
|
if (!parentFields) {
|
||||||
throw new Error(
|
throw new GraphqlQueryRunnerException(
|
||||||
`Could not find object metadata for ${parentObjectMetadata.nameSingular}`,
|
`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 { gql } from 'graphql-tag';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
|
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
GraphqlQueryRunnerException,
|
GraphqlQueryRunnerException,
|
||||||
GraphqlQueryRunnerExceptionCode,
|
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 { 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 { 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 { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||||
|
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WorkspaceSchemaFactory {
|
export class WorkspaceSchemaFactory {
|
||||||
@ -32,6 +35,7 @@ export class WorkspaceSchemaFactory {
|
|||||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||||
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
||||||
private readonly featureFlagService: FeatureFlagService,
|
private readonly featureFlagService: FeatureFlagService,
|
||||||
|
private readonly twentyConfigService: TwentyConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createGraphQLSchema(authContext: AuthContext): Promise<GraphQLSchema> {
|
async createGraphQLSchema(authContext: AuthContext): Promise<GraphQLSchema> {
|
||||||
@ -44,7 +48,10 @@ export class WorkspaceSchemaFactory {
|
|||||||
authContext.workspace.id,
|
authContext.workspace.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isNewRelationEnabled) {
|
if (
|
||||||
|
isNewRelationEnabled &&
|
||||||
|
this.twentyConfigService.get('NODE_ENV') !== NodeEnvironment.test
|
||||||
|
) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(
|
console.log(
|
||||||
chalk.yellow('🚧 New relation schema generation is enabled 🚧'),
|
chalk.yellow('🚧 New relation schema generation is enabled 🚧'),
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
|||||||
|
|
||||||
@Controller('rest/*')
|
@Controller('rest/*')
|
||||||
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
|
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
|
||||||
|
@UseFilters(RestApiExceptionFilter)
|
||||||
export class RestApiCoreController {
|
export class RestApiCoreController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly restApiCoreService: RestApiCoreService,
|
private readonly restApiCoreService: RestApiCoreService,
|
||||||
@ -38,15 +39,12 @@ export class RestApiCoreController {
|
|||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
async handleApiGet(@Req() request: Request, @Res() res: Response) {
|
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()
|
@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) {
|
async handleApiDelete(@Req() request: Request, @Res() res: Response) {
|
||||||
const result = await this.restApiCoreServiceV2.delete(request);
|
const result = await this.restApiCoreServiceV2.delete(request);
|
||||||
|
|
||||||
@ -54,7 +52,6 @@ export class RestApiCoreController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@UseFilters(RestApiExceptionFilter)
|
|
||||||
async handleApiPost(@Req() request: Request, @Res() res: Response) {
|
async handleApiPost(@Req() request: Request, @Res() res: Response) {
|
||||||
const result = await this.restApiCoreServiceV2.createOne(request);
|
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,
|
// 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.
|
// and because the PUT verb is often used as a PATCH.
|
||||||
@Put()
|
@Put()
|
||||||
@UseFilters(RestApiExceptionFilter)
|
|
||||||
async handleApiPut(@Req() request: Request, @Res() res: Response) {
|
async handleApiPut(@Req() request: Request, @Res() res: Response) {
|
||||||
const result = await this.restApiCoreServiceV2.update(request);
|
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 { Request } from 'express';
|
||||||
|
|
||||||
import { CreateManyQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/create-many-query.factory';
|
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 { 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 { 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 { 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 { 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';
|
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 { 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 { 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 { 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()
|
@Injectable()
|
||||||
export class CoreQueryBuilderFactory {
|
export class CoreQueryBuilderFactory {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly deleteQueryFactory: DeleteQueryFactory,
|
|
||||||
private readonly createOneQueryFactory: CreateOneQueryFactory,
|
|
||||||
private readonly createManyQueryFactory: CreateManyQueryFactory,
|
private readonly createManyQueryFactory: CreateManyQueryFactory,
|
||||||
private readonly updateQueryFactory: UpdateQueryFactory,
|
|
||||||
private readonly findOneQueryFactory: FindOneQueryFactory,
|
|
||||||
private readonly findManyQueryFactory: FindManyQueryFactory,
|
|
||||||
private readonly findDuplicatesQueryFactory: FindDuplicatesQueryFactory,
|
private readonly findDuplicatesQueryFactory: FindDuplicatesQueryFactory,
|
||||||
private readonly deleteVariablesFactory: DeleteVariablesFactory,
|
|
||||||
private readonly createVariablesFactory: CreateVariablesFactory,
|
private readonly createVariablesFactory: CreateVariablesFactory,
|
||||||
private readonly updateVariablesFactory: UpdateVariablesFactory,
|
|
||||||
private readonly getVariablesFactory: GetVariablesFactory,
|
|
||||||
private readonly findDuplicatesVariablesFactory: FindDuplicatesVariablesFactory,
|
private readonly findDuplicatesVariablesFactory: FindDuplicatesVariablesFactory,
|
||||||
private readonly accessTokenService: AccessTokenService,
|
private readonly accessTokenService: AccessTokenService,
|
||||||
private readonly domainManagerService: DomainManagerService,
|
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> {
|
async createMany(request: Request): Promise<Query> {
|
||||||
const { object: parsedObject } = parseCoreBatchPath(request);
|
const { object: parsedObject } = parseCoreBatchPath(request);
|
||||||
const objectMetadata = await this.getObjectMetadata(request, parsedObject);
|
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> {
|
async findDuplicates(request: Request): Promise<Query> {
|
||||||
const { object: parsedObject } = parseCorePath(request);
|
const { object: parsedObject } = parseCorePath(request);
|
||||||
const objectMetadata = await this.getObjectMetadata(request, parsedObject);
|
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 { 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 { 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 { 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 { 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 { CreateVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/create-variables.factory';
|
||||||
import { UpdateVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/update-variables.factory';
|
|
||||||
import { inputFactories } from 'src/engine/api/rest/input-factories/factories';
|
import { inputFactories } from 'src/engine/api/rest/input-factories/factories';
|
||||||
|
|
||||||
export const coreQueryBuilderFactories = [
|
export const coreQueryBuilderFactories = [
|
||||||
DeleteQueryFactory,
|
|
||||||
CreateOneQueryFactory,
|
|
||||||
CreateManyQueryFactory,
|
CreateManyQueryFactory,
|
||||||
UpdateQueryFactory,
|
|
||||||
FindOneQueryFactory,
|
|
||||||
FindManyQueryFactory,
|
|
||||||
FindDuplicatesQueryFactory,
|
FindDuplicatesQueryFactory,
|
||||||
DeleteVariablesFactory,
|
|
||||||
CreateVariablesFactory,
|
CreateVariablesFactory,
|
||||||
UpdateVariablesFactory,
|
|
||||||
GetVariablesFactory,
|
GetVariablesFactory,
|
||||||
FindDuplicatesVariablesFactory,
|
FindDuplicatesVariablesFactory,
|
||||||
...inputFactories,
|
...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 } } };
|
return { filter: { id: { eq: id } } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filter = this.filterInputFactory.create(request, objectMetadata);
|
||||||
const limit = this.limitInputFactory.create(request);
|
const limit = this.limitInputFactory.create(request);
|
||||||
|
const orderBy = this.orderByInputFactory.create(request, objectMetadata);
|
||||||
const endingBefore = this.endingBeforeInputFactory.create(request);
|
const endingBefore = this.endingBeforeInputFactory.create(request);
|
||||||
const startingAfter = this.startingAfterInputFactory.create(request);
|
const startingAfter = this.startingAfterInputFactory.create(request);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filter: this.filterInputFactory.create(request, objectMetadata),
|
filter,
|
||||||
orderBy: this.orderByInputFactory.create(request, objectMetadata),
|
orderBy,
|
||||||
first: !endingBefore ? limit : undefined,
|
first: !endingBefore ? limit : undefined,
|
||||||
last: endingBefore ? limit : undefined,
|
last: endingBefore ? limit : undefined,
|
||||||
startingAfter,
|
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 { 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 { 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 { RestApiDeleteOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-delete-one.handler';
|
||||||
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
|
import { RestApiCreateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-one.handler';
|
||||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler';
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
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()
|
@Injectable()
|
||||||
export class RestApiCoreServiceV2 {
|
export class RestApiCoreServiceV2 {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly coreQueryBuilderFactory: CoreQueryBuilderFactory,
|
private readonly restApiDeleteOneHandler: RestApiDeleteOneHandler,
|
||||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
private readonly restApiCreateOneHandler: RestApiCreateOneHandler,
|
||||||
private readonly recordInputTransformerService: RecordInputTransformerService,
|
private readonly restApiUpdateOneHandler: RestApiUpdateOneHandler,
|
||||||
protected readonly apiEventEmitterService: ApiEventEmitterService,
|
private readonly restApiGetOneHandler: RestApiGetOneHandler,
|
||||||
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
|
private readonly restApiGetManyHandler: RestApiGetManyHandler,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async delete(request: Request) {
|
async delete(request: Request) {
|
||||||
const { id: recordId } = parseCorePath(request);
|
return await this.restApiDeleteOneHandler.handle(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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOne(request: Request) {
|
async createOne(request: Request) {
|
||||||
const { objectMetadataNameSingular, objectMetadata, repository } =
|
return await this.restApiCreateOneHandler.handle(request);
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(request: Request) {
|
async update(request: Request) {
|
||||||
|
return await this.restApiUpdateOneHandler.handle(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(request: Request) {
|
||||||
const { id: recordId } = parseCorePath(request);
|
const { id: recordId } = parseCorePath(request);
|
||||||
|
|
||||||
if (!recordId) {
|
if (isDefined(recordId)) {
|
||||||
throw new BadRequestException('Record ID not found');
|
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,
|
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) {
|
async createMany(request: Request) {
|
||||||
const data = await this.coreQueryBuilderFactory.createMany(request);
|
const data = await this.coreQueryBuilderFactory.createMany(request);
|
||||||
|
|
||||||
return await this.restApiService.call(GraphqlApiType.CORE, request, data);
|
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) {
|
async findDuplicates(request: Request) {
|
||||||
const data = await this.coreQueryBuilderFactory.findDuplicates(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 = {
|
export type QueryVariables = {
|
||||||
id?: string;
|
id?: string;
|
||||||
ids?: string[];
|
ids?: string[];
|
||||||
data?: object | null;
|
data?: object | null;
|
||||||
filter?: object;
|
filter?: object;
|
||||||
orderBy?: object;
|
orderBy?: ObjectRecordOrderBy;
|
||||||
last?: number;
|
last?: number;
|
||||||
first?: number;
|
first?: number;
|
||||||
startingAfter?: string;
|
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 { 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 { 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 { 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 = [
|
export const inputFactories = [
|
||||||
StartingAfterInputFactory,
|
DepthInputFactory,
|
||||||
EndingBeforeInputFactory,
|
EndingBeforeInputFactory,
|
||||||
|
FilterInputFactory,
|
||||||
LimitInputFactory,
|
LimitInputFactory,
|
||||||
OrderByInputFactory,
|
OrderByInputFactory,
|
||||||
FilterInputFactory,
|
StartingAfterInputFactory,
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,51 +1,23 @@
|
|||||||
import { HttpModule } from '@nestjs/axios';
|
import { HttpModule } from '@nestjs/axios';
|
||||||
import { Module } from '@nestjs/common';
|
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 { 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 { 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 { 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 { 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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
CoreQueryBuilderModule,
|
|
||||||
MetadataQueryBuilderModule,
|
MetadataQueryBuilderModule,
|
||||||
WorkspaceCacheStorageModule,
|
WorkspaceCacheStorageModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
HttpModule,
|
HttpModule,
|
||||||
TwentyORMModule,
|
RestApiCoreModule,
|
||||||
RecordTransformerModule,
|
|
||||||
WorkspacePermissionsCacheModule,
|
|
||||||
],
|
],
|
||||||
controllers: [
|
controllers: [RestApiMetadataController],
|
||||||
RestApiMetadataController,
|
providers: [RestApiService, RestApiMetadataService],
|
||||||
RestApiCoreBatchController,
|
|
||||||
RestApiCoreController,
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
RestApiMetadataService,
|
|
||||||
RestApiCoreService,
|
|
||||||
RestApiCoreServiceV2,
|
|
||||||
RestApiService,
|
|
||||||
StartingAfterInputFactory,
|
|
||||||
EndingBeforeInputFactory,
|
|
||||||
LimitInputFactory,
|
|
||||||
ApiEventEmitterService,
|
|
||||||
],
|
|
||||||
exports: [RestApiMetadataService],
|
|
||||||
})
|
})
|
||||||
export class RestApiModule {}
|
export class RestApiModule {}
|
||||||
|
|||||||
@ -15,15 +15,15 @@ export class CacheStorageService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async get<T>(key: string): Promise<T | undefined> {
|
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) {
|
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) {
|
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) {
|
async setAdd(key: string, value: string[], ttl?: Milliseconds) {
|
||||||
@ -33,13 +33,13 @@ export class CacheStorageService {
|
|||||||
|
|
||||||
if (this.isRedisCache()) {
|
if (this.isRedisCache()) {
|
||||||
await (this.cache as RedisCache).store.client.sAdd(
|
await (this.cache as RedisCache).store.client.sAdd(
|
||||||
`${this.namespace}:${key}`,
|
this.getKey(key),
|
||||||
value,
|
value,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (ttl) {
|
if (ttl) {
|
||||||
await (this.cache as RedisCache).store.client.expire(
|
await (this.cache as RedisCache).store.client.expire(
|
||||||
`${this.namespace}:${key}`,
|
this.getKey(key),
|
||||||
ttl / 1000,
|
ttl / 1000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -65,7 +65,7 @@ export class CacheStorageService {
|
|||||||
async setPop(key: string, size = 1) {
|
async setPop(key: string, size = 1) {
|
||||||
if (this.isRedisCache()) {
|
if (this.isRedisCache()) {
|
||||||
return (this.cache as RedisCache).store.client.sPop(
|
return (this.cache as RedisCache).store.client.sPop(
|
||||||
`${this.namespace}:${key}`,
|
this.getKey(key),
|
||||||
size,
|
size,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -84,7 +84,7 @@ export class CacheStorageService {
|
|||||||
async getSetLength(key: string) {
|
async getSetLength(key: string) {
|
||||||
if (this.isRedisCache()) {
|
if (this.isRedisCache()) {
|
||||||
return await (this.cache as RedisCache).store.client.sCard(
|
return await (this.cache as RedisCache).store.client.sCard(
|
||||||
`${this.namespace}:${key}`,
|
this.getKey(key),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,4 +125,14 @@ export class CacheStorageService {
|
|||||||
private isRedisCache() {
|
private isRedisCache() {
|
||||||
return (this.cache.store as any)?.name === 'redis';
|
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;
|
data: U;
|
||||||
};
|
};
|
||||||
|
|
||||||
const USER_WORKSPACE_ROLE_MAP = 'User workspace role map';
|
export const USER_WORKSPACE_ROLE_MAP = 'User workspace role map';
|
||||||
const ROLES_PERMISSIONS = 'Roles permissions';
|
export const ROLES_PERMISSIONS = 'Roles permissions';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WorkspacePermissionsCacheService {
|
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 { 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 { 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 { 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 { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
|
||||||
import {
|
import {
|
||||||
TwentyORMException,
|
TwentyORMException,
|
||||||
@ -229,7 +232,7 @@ export class WorkspaceDatasourceFactory {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
ignoreLock: true,
|
ignoreLock: true,
|
||||||
}),
|
}),
|
||||||
cachedEntityName: 'Roles permissions',
|
cachedEntityName: ROLES_PERMISSIONS,
|
||||||
exceptionCode: TwentyORMExceptionCode.ROLES_PERMISSIONS_VERSION_NOT_FOUND,
|
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 {
|
import {
|
||||||
PERSON_1_ID,
|
TEST_PERSON_1_ID,
|
||||||
PERSON_2_ID,
|
TEST_PERSON_2_ID,
|
||||||
PERSON_3_ID,
|
TEST_PERSON_3_ID,
|
||||||
} from 'test/integration/constants/mock-person-ids.constants';
|
} from 'test/integration/constants/test-person-ids.constants';
|
||||||
import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.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 { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util';
|
||||||
import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-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 { updateManyOperationFactory } from 'test/integration/graphql/utils/update-many-operation-factory.util';
|
||||||
import { updateOneOperationFactory } from 'test/integration/graphql/utils/update-one-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 { generateRecordName } from 'test/integration/utils/generate-record-name';
|
||||||
|
import { deleteAllRecords } from 'test/integration/utils/delete-all-records';
|
||||||
|
|
||||||
describe('people resolvers (integration)', () => {
|
describe('people resolvers (integration)', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await deleteAllRecords('person');
|
||||||
|
});
|
||||||
|
|
||||||
it('1. should create and return people', async () => {
|
it('1. should create and return people', async () => {
|
||||||
const personCity1 = generateRecordName(PERSON_1_ID);
|
const personCity1 = generateRecordName(TEST_PERSON_1_ID);
|
||||||
const personCity2 = generateRecordName(PERSON_2_ID);
|
const personCity2 = generateRecordName(TEST_PERSON_2_ID);
|
||||||
const graphqlOperation = createManyOperationFactory({
|
const graphqlOperation = createManyOperationFactory({
|
||||||
objectMetadataSingularName: 'person',
|
objectMetadataSingularName: 'person',
|
||||||
objectMetadataPluralName: 'people',
|
objectMetadataPluralName: 'people',
|
||||||
gqlFields: PERSON_GQL_FIELDS,
|
gqlFields: PERSON_GQL_FIELDS,
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
id: PERSON_1_ID,
|
id: TEST_PERSON_1_ID,
|
||||||
city: personCity1,
|
city: personCity1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: PERSON_2_ID,
|
id: TEST_PERSON_2_ID,
|
||||||
city: personCity2,
|
city: personCity2,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -57,13 +62,13 @@ describe('people resolvers (integration)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('1b. should create and return one person', async () => {
|
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({
|
const graphqlOperation = createOneOperationFactory({
|
||||||
objectMetadataSingularName: 'person',
|
objectMetadataSingularName: 'person',
|
||||||
gqlFields: PERSON_GQL_FIELDS,
|
gqlFields: PERSON_GQL_FIELDS,
|
||||||
data: {
|
data: {
|
||||||
id: PERSON_3_ID,
|
id: TEST_PERSON_3_ID,
|
||||||
city: personCity3,
|
city: personCity3,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -121,7 +126,7 @@ describe('people resolvers (integration)', () => {
|
|||||||
gqlFields: PERSON_GQL_FIELDS,
|
gqlFields: PERSON_GQL_FIELDS,
|
||||||
filter: {
|
filter: {
|
||||||
id: {
|
id: {
|
||||||
eq: PERSON_3_ID,
|
eq: TEST_PERSON_3_ID,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -152,7 +157,7 @@ describe('people resolvers (integration)', () => {
|
|||||||
},
|
},
|
||||||
filter: {
|
filter: {
|
||||||
id: {
|
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: {
|
data: {
|
||||||
city: 'New City',
|
city: 'New City',
|
||||||
},
|
},
|
||||||
recordId: PERSON_3_ID,
|
recordId: TEST_PERSON_3_ID,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||||
@ -225,7 +230,7 @@ describe('people resolvers (integration)', () => {
|
|||||||
gqlFields: PERSON_GQL_FIELDS,
|
gqlFields: PERSON_GQL_FIELDS,
|
||||||
filter: {
|
filter: {
|
||||||
id: {
|
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({
|
const graphqlOperation = deleteOneOperationFactory({
|
||||||
objectMetadataSingularName: 'person',
|
objectMetadataSingularName: 'person',
|
||||||
gqlFields: PERSON_GQL_FIELDS,
|
gqlFields: PERSON_GQL_FIELDS,
|
||||||
recordId: PERSON_3_ID,
|
recordId: TEST_PERSON_3_ID,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||||
@ -260,7 +265,7 @@ describe('people resolvers (integration)', () => {
|
|||||||
gqlFields: PERSON_GQL_FIELDS,
|
gqlFields: PERSON_GQL_FIELDS,
|
||||||
filter: {
|
filter: {
|
||||||
id: {
|
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,
|
gqlFields: PERSON_GQL_FIELDS,
|
||||||
filter: {
|
filter: {
|
||||||
id: {
|
id: {
|
||||||
eq: PERSON_3_ID,
|
eq: TEST_PERSON_3_ID,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -293,7 +298,7 @@ describe('people resolvers (integration)', () => {
|
|||||||
gqlFields: PERSON_GQL_FIELDS,
|
gqlFields: PERSON_GQL_FIELDS,
|
||||||
filter: {
|
filter: {
|
||||||
id: {
|
id: {
|
||||||
in: [PERSON_1_ID, PERSON_2_ID],
|
in: [TEST_PERSON_1_ID, TEST_PERSON_2_ID],
|
||||||
},
|
},
|
||||||
not: {
|
not: {
|
||||||
deletedAt: {
|
deletedAt: {
|
||||||
@ -314,7 +319,7 @@ describe('people resolvers (integration)', () => {
|
|||||||
gqlFields: PERSON_GQL_FIELDS,
|
gqlFields: PERSON_GQL_FIELDS,
|
||||||
filter: {
|
filter: {
|
||||||
id: {
|
id: {
|
||||||
eq: PERSON_3_ID,
|
eq: TEST_PERSON_3_ID,
|
||||||
},
|
},
|
||||||
not: {
|
not: {
|
||||||
deletedAt: {
|
deletedAt: {
|
||||||
@ -326,7 +331,7 @@ describe('people resolvers (integration)', () => {
|
|||||||
|
|
||||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
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 () => {
|
it('8. should destroy many people', async () => {
|
||||||
@ -336,7 +341,7 @@ describe('people resolvers (integration)', () => {
|
|||||||
gqlFields: PERSON_GQL_FIELDS,
|
gqlFields: PERSON_GQL_FIELDS,
|
||||||
filter: {
|
filter: {
|
||||||
id: {
|
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({
|
const graphqlOperation = destroyOneOperationFactory({
|
||||||
objectMetadataSingularName: 'person',
|
objectMetadataSingularName: 'person',
|
||||||
gqlFields: PERSON_GQL_FIELDS,
|
gqlFields: PERSON_GQL_FIELDS,
|
||||||
recordId: PERSON_3_ID,
|
recordId: TEST_PERSON_3_ID,
|
||||||
});
|
});
|
||||||
|
|
||||||
const destroyPeopleResponse = await makeGraphqlAPIRequest(graphqlOperation);
|
const destroyPeopleResponse = await makeGraphqlAPIRequest(graphqlOperation);
|
||||||
@ -365,7 +370,7 @@ describe('people resolvers (integration)', () => {
|
|||||||
gqlFields: PERSON_GQL_FIELDS,
|
gqlFields: PERSON_GQL_FIELDS,
|
||||||
filter: {
|
filter: {
|
||||||
id: {
|
id: {
|
||||||
in: [PERSON_1_ID, PERSON_2_ID],
|
in: [TEST_PERSON_1_ID, TEST_PERSON_2_ID],
|
||||||
},
|
},
|
||||||
not: {
|
not: {
|
||||||
deletedAt: {
|
deletedAt: {
|
||||||
@ -386,7 +391,7 @@ describe('people resolvers (integration)', () => {
|
|||||||
gqlFields: PERSON_GQL_FIELDS,
|
gqlFields: PERSON_GQL_FIELDS,
|
||||||
filter: {
|
filter: {
|
||||||
id: {
|
id: {
|
||||||
eq: PERSON_3_ID,
|
eq: TEST_PERSON_3_ID,
|
||||||
},
|
},
|
||||||
not: {
|
not: {
|
||||||
deletedAt: {
|
deletedAt: {
|
||||||
|
|||||||
@ -8,7 +8,7 @@ const ORIGIN = new URL(SERVER_URL);
|
|||||||
|
|
||||||
ORIGIN.hostname =
|
ORIGIN.hostname =
|
||||||
process.env.IS_MULTIWORKSPACE_ENABLED === 'true'
|
process.env.IS_MULTIWORKSPACE_ENABLED === 'true'
|
||||||
? `acme.${ORIGIN.hostname}`
|
? `apple.${ORIGIN.hostname}`
|
||||||
: ORIGIN.hostname;
|
: ORIGIN.hostname;
|
||||||
|
|
||||||
const auth = {
|
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';
|
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||||
|
|
||||||
describe('datamodel permissions', () => {
|
describe('datamodel permissions', () => {
|
||||||
beforeAll(async () => {});
|
|
||||||
|
|
||||||
describe('fieldMetadata', () => {
|
describe('fieldMetadata', () => {
|
||||||
let listingObjectId = '';
|
let listingObjectId = '';
|
||||||
let testFieldId = '';
|
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 { 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 { 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', () => {
|
describe('Core REST API Create One endpoint', () => {
|
||||||
beforeAll(
|
beforeEach(async () => {
|
||||||
async () =>
|
await deleteAllRecords('person');
|
||||||
await makeRestAPIRequest({
|
await makeRestAPIRequest({
|
||||||
method: 'delete',
|
method: 'post',
|
||||||
path: `/people/${PERSON_1_ID}`,
|
path: '/companies',
|
||||||
}),
|
body: {
|
||||||
);
|
id: TEST_COMPANY_1_ID,
|
||||||
|
domainName: {
|
||||||
|
primaryLinkUrl: TEST_PRIMARY_LINK_URL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should create a new person', async () => {
|
it('should create a new person', async () => {
|
||||||
const personCity = generateRecordName(PERSON_1_ID);
|
const personCity = generateRecordName(TEST_PERSON_1_ID);
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
id: PERSON_1_ID,
|
id: TEST_PERSON_1_ID,
|
||||||
city: personCity,
|
city: personCity,
|
||||||
|
companyId: TEST_COMPANY_1_ID,
|
||||||
};
|
};
|
||||||
|
|
||||||
await makeRestAPIRequest({
|
await makeRestAPIRequest({
|
||||||
@ -27,18 +37,97 @@ describe('Core REST API Create One endpoint', () => {
|
|||||||
.expect((res) => {
|
.expect((res) => {
|
||||||
const createdPerson = res.body.data.createPerson;
|
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);
|
expect(createdPerson.city).toBe(personCity);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return a BadRequestException when trying to create a person with an existing ID', async () => {
|
it('should support depth 0 parameter', async () => {
|
||||||
const personCity = generateRecordName(PERSON_1_ID);
|
const personCity = generateRecordName(TEST_PERSON_1_ID);
|
||||||
const requestBody = {
|
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,
|
city: personCity,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
await makeRestAPIRequest({
|
||||||
|
method: 'post',
|
||||||
|
path: `/people`,
|
||||||
|
body: requestBody,
|
||||||
|
});
|
||||||
|
|
||||||
await makeRestAPIRequest({
|
await makeRestAPIRequest({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
path: `/people`,
|
path: `/people`,
|
||||||
|
|||||||
@ -1,34 +1,59 @@
|
|||||||
import {
|
import {
|
||||||
NOT_EXISTING_PERSON_ID,
|
NOT_EXISTING_TEST_PERSON_ID,
|
||||||
PERSON_1_ID,
|
TEST_PERSON_1_ID,
|
||||||
} from 'test/integration/constants/mock-person-ids.constants';
|
} from 'test/integration/constants/test-person-ids.constants';
|
||||||
import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util';
|
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', () => {
|
describe('Core REST API Delete One endpoint', () => {
|
||||||
beforeAll(
|
beforeAll(async () => {
|
||||||
async () =>
|
await deleteAllRecords('person');
|
||||||
await makeRestAPIRequest({
|
});
|
||||||
method: 'post',
|
|
||||||
path: `/people`,
|
beforeEach(async () => {
|
||||||
body: {
|
await makeRestAPIRequest({
|
||||||
id: PERSON_1_ID,
|
method: 'post',
|
||||||
},
|
path: `/people`,
|
||||||
}),
|
body: {
|
||||||
);
|
id: TEST_PERSON_1_ID,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should delete one person', async () => {
|
it('should delete one person', async () => {
|
||||||
await makeRestAPIRequest({
|
await makeRestAPIRequest({
|
||||||
method: 'delete',
|
method: 'delete',
|
||||||
path: `/people/${PERSON_1_ID}`,
|
path: `/people/${TEST_PERSON_1_ID}`,
|
||||||
})
|
})
|
||||||
.expect(200)
|
.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 () => {
|
it('should return a EntityNotFoundError when trying to delete a non-existing person', async () => {
|
||||||
await makeRestAPIRequest({
|
await makeRestAPIRequest({
|
||||||
method: 'delete',
|
method: 'delete',
|
||||||
path: `/people/${NOT_EXISTING_PERSON_ID}`,
|
path: `/people/${NOT_EXISTING_TEST_PERSON_ID}`,
|
||||||
})
|
})
|
||||||
.expect(400)
|
.expect(400)
|
||||||
.expect((res) => {
|
.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 {
|
import {
|
||||||
NOT_EXISTING_PERSON_ID,
|
NOT_EXISTING_TEST_PERSON_ID,
|
||||||
PERSON_1_ID,
|
TEST_PERSON_1_ID,
|
||||||
} from 'test/integration/constants/mock-person-ids.constants';
|
} from 'test/integration/constants/test-person-ids.constants';
|
||||||
import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util';
|
import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util';
|
||||||
import { generateRecordName } from 'test/integration/utils/generate-record-name';
|
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', () => {
|
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 () => {
|
beforeAll(async () => {
|
||||||
|
await deleteAllRecords('person');
|
||||||
await makeRestAPIRequest({
|
await makeRestAPIRequest({
|
||||||
method: 'delete',
|
method: 'post',
|
||||||
path: `/people/${PERSON_1_ID}`,
|
path: '/companies',
|
||||||
|
body: {
|
||||||
|
id: TEST_COMPANY_1_ID,
|
||||||
|
domainName: {
|
||||||
|
primaryLinkUrl: TEST_PRIMARY_LINK_URL,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
await makeRestAPIRequest({
|
await makeRestAPIRequest({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
path: `/people`,
|
path: `/people`,
|
||||||
body: {
|
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 () => {
|
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({
|
await makeRestAPIRequest({
|
||||||
method: 'patch',
|
method: 'patch',
|
||||||
path: `/people/${PERSON_1_ID}`,
|
path: `/people/${TEST_PERSON_1_ID}`,
|
||||||
body: updatedData,
|
body: updatedData,
|
||||||
})
|
})
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.expect((res) => {
|
.expect((res) => {
|
||||||
const updatedPerson = res.body.data.updatePerson;
|
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.firstName).toBe(updatedData.name.firstName);
|
||||||
expect(updatedPerson.name.lastName).toBe(updatedData.name.lastName);
|
expect(updatedPerson.name.lastName).toBe(updatedData.name.lastName);
|
||||||
expect(updatedPerson.emails.primaryEmail).toBe(
|
expect(updatedPerson.emails.primaryEmail).toBe(
|
||||||
@ -54,14 +65,67 @@ describe('Core REST API Update One endpoint', () => {
|
|||||||
expect(updatedPerson.city).toBe(updatedData.city);
|
expect(updatedPerson.city).toBe(updatedData.city);
|
||||||
|
|
||||||
expect(updatedPerson.jobTitle).toBe('');
|
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 () => {
|
it('should return a EntityNotFoundError when trying to update a non-existing person', async () => {
|
||||||
await makeRestAPIRequest({
|
await makeRestAPIRequest({
|
||||||
method: 'patch',
|
method: 'patch',
|
||||||
path: `/people/${NOT_EXISTING_PERSON_ID}`,
|
path: `/people/${NOT_EXISTING_TEST_PERSON_ID}`,
|
||||||
})
|
})
|
||||||
.expect(400)
|
.expect(400)
|
||||||
.expect((res) => {
|
.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 { JestConfigWithTsJest } from 'ts-jest';
|
||||||
import 'tsconfig-paths/register';
|
import 'tsconfig-paths/register';
|
||||||
|
|
||||||
|
import { rawDataSource } from 'src/database/typeorm/raw/raw.datasource';
|
||||||
|
|
||||||
import { createApp } from './create-app';
|
import { createApp } from './create-app';
|
||||||
|
|
||||||
export default async (_, projectConfig: JestConfigWithTsJest) => {
|
export default async (_, projectConfig: JestConfigWithTsJest) => {
|
||||||
@ -10,7 +12,10 @@ export default async (_, projectConfig: JestConfigWithTsJest) => {
|
|||||||
throw new Error('No globals found in project config');
|
throw new Error('No globals found in project config');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await rawDataSource.initialize();
|
||||||
|
|
||||||
await app.listen(projectConfig.globals.APP_PORT);
|
await app.listen(projectConfig.globals.APP_PORT);
|
||||||
|
|
||||||
global.app = app;
|
global.app = app;
|
||||||
|
global.testDataSource = rawDataSource;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import 'tsconfig-paths/register';
|
import 'tsconfig-paths/register';
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
|
global.testDataSource.destroy();
|
||||||
global.app.close();
|
global.app.close();
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user