4655 batch endpoints on the rest api (#5411)

- add POST rest/batch/<OBJECT> endpoint
- rearrange rest api code with Twenty quality standard
- unify REST API error format
- Added PATCH verb to update objects
- In openapi schema, we replaced PUT with PATCH verb to comply with REST
standard
- fix openApi schema to match the REST api

### Batch Create

![image](https://github.com/twentyhq/twenty/assets/29927851/fe8cd91d-7b35-477f-9077-3477b57b054c)

### Replace PUT by PATCH in open Api

![image](https://github.com/twentyhq/twenty/assets/29927851/9a95060d-0b21-4a04-a3fa-c53390897b5b)

### Error format unification

![image](https://github.com/twentyhq/twenty/assets/29927851/f47dfcef-a4f8-4f93-8504-22f82a8d8057)

![image](https://github.com/twentyhq/twenty/assets/29927851/d76a87e2-2bf6-4ed9-a142-71ad7c123beb)

![image](https://github.com/twentyhq/twenty/assets/29927851/6db59ad3-0ba7-4390-a02d-be15884e2516)
This commit is contained in:
martmull
2024-05-16 14:15:49 +02:00
committed by GitHub
parent ea5a7ba70e
commit fdf10f17e2
90 changed files with 1318 additions and 857 deletions

View File

@ -1,40 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { HttpService } from '@nestjs/axios';
import { ApiRestService } from 'src/engine/api/rest/api-rest.service';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { ApiRestQueryBuilderFactory } from 'src/engine/api/rest/api-rest-query-builder/api-rest-query-builder.factory';
describe('ApiRestService', () => {
let service: ApiRestService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ApiRestService,
{
provide: ApiRestQueryBuilderFactory,
useValue: {},
},
{
provide: EnvironmentService,
useValue: {},
},
{
provide: TokenService,
useValue: {},
},
{
provide: HttpService,
useValue: {},
},
],
}).compile();
service = module.get<ApiRestService>(ApiRestService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -1,46 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ApiRestQueryBuilderFactory } from 'src/engine/api/rest/api-rest-query-builder/api-rest-query-builder.factory';
import { DeleteQueryFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/delete-query.factory';
import { CreateQueryFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/create-query.factory';
import { UpdateQueryFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/update-query.factory';
import { FindOneQueryFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/find-one-query.factory';
import { FindManyQueryFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/find-many-query.factory';
import { DeleteVariablesFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/delete-variables.factory';
import { CreateVariablesFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/create-variables.factory';
import { UpdateVariablesFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/update-variables.factory';
import { GetVariablesFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/get-variables.factory';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
describe('ApiRestQueryBuilderFactory', () => {
let service: ApiRestQueryBuilderFactory;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ApiRestQueryBuilderFactory,
{ provide: DeleteQueryFactory, useValue: {} },
{ provide: CreateQueryFactory, useValue: {} },
{ provide: UpdateQueryFactory, useValue: {} },
{ provide: FindOneQueryFactory, useValue: {} },
{ provide: FindManyQueryFactory, useValue: {} },
{ provide: DeleteVariablesFactory, useValue: {} },
{ provide: CreateVariablesFactory, useValue: {} },
{ provide: UpdateVariablesFactory, useValue: {} },
{ provide: GetVariablesFactory, useValue: {} },
{ provide: ObjectMetadataService, useValue: {} },
{ provide: TokenService, useValue: {} },
{ provide: EnvironmentService, useValue: {} },
],
}).compile();
service = module.get<ApiRestQueryBuilderFactory>(
ApiRestQueryBuilderFactory,
);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -1,13 +0,0 @@
import { Module } from '@nestjs/common';
import { ApiRestQueryBuilderFactory } from 'src/engine/api/rest/api-rest-query-builder/api-rest-query-builder.factory';
import { apiRestQueryBuilderFactories } from 'src/engine/api/rest/api-rest-query-builder/factories/factories';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
@Module({
imports: [ObjectMetadataModule, AuthModule],
providers: [...apiRestQueryBuilderFactories, ApiRestQueryBuilderFactory],
exports: [ApiRestQueryBuilderFactory],
})
export class ApiRestQueryBuilderModule {}

View File

@ -1,14 +0,0 @@
import { Injectable } from '@nestjs/common';
import { Request } from 'express';
import { ApiRestQueryVariables } from 'src/engine/api/rest/types/api-rest-query-variables.type';
@Injectable()
export class CreateVariablesFactory {
create(request: Request): ApiRestQueryVariables {
return {
data: request.body,
};
}
}

View File

@ -1,12 +0,0 @@
import { Injectable } from '@nestjs/common';
import { ApiRestQueryVariables } from 'src/engine/api/rest/types/api-rest-query-variables.type';
@Injectable()
export class DeleteVariablesFactory {
create(id: string): ApiRestQueryVariables {
return {
id: id,
};
}
}

View File

@ -1,29 +0,0 @@
import { DeleteQueryFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/delete-query.factory';
import { CreateQueryFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/create-query.factory';
import { UpdateQueryFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/update-query.factory';
import { FindOneQueryFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/find-one-query.factory';
import { FindManyQueryFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/find-many-query.factory';
import { DeleteVariablesFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/delete-variables.factory';
import { CreateVariablesFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/create-variables.factory';
import { UpdateVariablesFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/update-variables.factory';
import { GetVariablesFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/get-variables.factory';
import { LastCursorInputFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/last-cursor-input.factory';
import { LimitInputFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/limit-input.factory';
import { OrderByInputFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/order-by-input.factory';
import { FilterInputFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-input.factory';
export const apiRestQueryBuilderFactories = [
DeleteQueryFactory,
CreateQueryFactory,
UpdateQueryFactory,
FindOneQueryFactory,
FindManyQueryFactory,
DeleteVariablesFactory,
CreateVariablesFactory,
UpdateVariablesFactory,
GetVariablesFactory,
LastCursorInputFactory,
LimitInputFactory,
OrderByInputFactory,
FilterInputFactory,
];

View File

@ -1,32 +0,0 @@
import { objectMetadataItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock';
import {
checkFields,
getFieldType,
} from 'src/engine/api/rest/api-rest-query-builder/utils/fields.utils';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
describe('FieldUtils', () => {
describe('getFieldType', () => {
it('should get field type', () => {
expect(getFieldType(objectMetadataItemMock, 'fieldNumber')).toEqual(
FieldMetadataType.NUMBER,
);
});
});
describe('checkFields', () => {
it('should check field types', () => {
expect(() =>
checkFields(objectMetadataItemMock, ['fieldNumber']),
).not.toThrow();
expect(() =>
checkFields(objectMetadataItemMock, ['wrongField']),
).toThrow();
expect(() =>
checkFields(objectMetadataItemMock, ['fieldNumber', 'wrongField']),
).toThrow();
});
});
});

View File

@ -1,21 +0,0 @@
import { parsePath } from 'src/engine/api/rest/api-rest-query-builder/utils/parse-path.utils';
describe('parsePath', () => {
it('should parse object from request path', () => {
const request: any = { path: '/rest/companies/uuid' };
expect(parsePath(request)).toEqual({
object: 'companies',
id: 'uuid',
});
});
it('should parse object from request path', () => {
const request: any = { path: '/rest/companies' };
expect(parsePath(request)).toEqual({
object: 'companies',
id: undefined,
});
});
});

View File

@ -1,39 +0,0 @@
import { Controller, Delete, Get, Post, Put, Req, Res } from '@nestjs/common';
import { Request, Response } from 'express';
import { ApiRestService } from 'src/engine/api/rest/api-rest.service';
import { cleanGraphQLResponse } from 'src/engine/api/rest/api-rest.controller.utils';
@Controller('rest/*')
export class ApiRestController {
constructor(private readonly apiRestService: ApiRestService) {}
@Get()
async handleApiGet(@Req() request: Request, @Res() res: Response) {
const result = await this.apiRestService.get(request);
res.send(cleanGraphQLResponse(result.data));
}
@Delete()
async handleApiDelete(@Req() request: Request, @Res() res: Response) {
const result = await this.apiRestService.delete(request);
res.send(cleanGraphQLResponse(result.data));
}
@Post()
async handleApiPost(@Req() request: Request, @Res() res: Response) {
const result = await this.apiRestService.create(request);
res.send(cleanGraphQLResponse(result.data));
}
@Put()
async handleApiPut(@Req() request: Request, @Res() res: Response) {
const result = await this.apiRestService.update(request);
res.send(cleanGraphQLResponse(result.data));
}
}

View File

@ -1,17 +0,0 @@
import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { ApiRestController } from 'src/engine/api/rest/api-rest.controller';
import { ApiRestService } from 'src/engine/api/rest/api-rest.service';
import { ApiRestQueryBuilderModule } from 'src/engine/api/rest/api-rest-query-builder/api-rest-query-builder.module';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { ApiRestMetadataController } from 'src/engine/api/rest/metadata-rest.controller';
import { ApiRestMetadataService } from 'src/engine/api/rest/metadata-rest.service';
@Module({
imports: [ApiRestQueryBuilderModule, AuthModule, HttpModule],
controllers: [ApiRestMetadataController, ApiRestController],
providers: [ApiRestMetadataService, ApiRestService],
exports: [ApiRestMetadataService],
})
export class ApiRestModule {}

View File

@ -1,72 +0,0 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { Request } from 'express';
import { AxiosResponse } from 'axios';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { ApiRestQueryBuilderFactory } from 'src/engine/api/rest/api-rest-query-builder/api-rest-query-builder.factory';
import { ApiRestQuery } from 'src/engine/api/rest/types/api-rest-query.type';
import { getServerUrl } from 'src/utils/get-server-url';
@Injectable()
export class ApiRestService {
constructor(
private readonly environmentService: EnvironmentService,
private readonly apiRestQueryBuilderFactory: ApiRestQueryBuilderFactory,
private readonly httpService: HttpService,
) {}
async callGraphql(request: Request, data: ApiRestQuery) {
const baseUrl = getServerUrl(
request,
this.environmentService.get('SERVER_URL'),
);
let response: AxiosResponse;
try {
response = await this.httpService.axiosRef.post(
`${baseUrl}/graphql`,
data,
{
headers: {
'Content-Type': 'application/json',
Authorization: request.headers.authorization,
},
},
);
} catch (err) {
throw new BadRequestException(err.response.data);
}
if (response.data.errors?.length) {
throw new BadRequestException(response.data);
}
return response;
}
async get(request: Request) {
const data = await this.apiRestQueryBuilderFactory.get(request);
return await this.callGraphql(request, data);
}
async delete(request: Request) {
const data = await this.apiRestQueryBuilderFactory.delete(request);
return await this.callGraphql(request, data);
}
async create(request: Request) {
const data = await this.apiRestQueryBuilderFactory.create(request);
return await this.callGraphql(request, data);
}
async update(request: Request) {
const data = await this.apiRestQueryBuilderFactory.update(request);
return await this.callGraphql(request, data);
}
}

View File

@ -0,0 +1,18 @@
import { Controller, Post, Req, Res } from '@nestjs/common';
import { Request, Response } from 'express';
import { RestApiCoreService } from 'src/engine/api/rest/services/rest-api-core.service';
import { cleanGraphQLResponse } from 'src/engine/api/rest/utils/clean-graphql-response.utils';
@Controller('rest/batch/*')
export class RestApiCoreBatchController {
constructor(private readonly restApiCoreService: RestApiCoreService) {}
@Post()
async handleApiPost(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiCoreService.createMany(request);
res.status(201).send(cleanGraphQLResponse(result.data));
}
}

View File

@ -0,0 +1,58 @@
import {
Controller,
Delete,
Get,
Patch,
Post,
Put,
Req,
Res,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { RestApiCoreService } from 'src/engine/api/rest/services/rest-api-core.service';
import { cleanGraphQLResponse } from 'src/engine/api/rest/utils/clean-graphql-response.utils';
@Controller('rest/*')
export class RestApiCoreController {
constructor(private readonly restApiCoreService: RestApiCoreService) {}
@Get()
async handleApiGet(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiCoreService.get(request);
res.status(200).send(cleanGraphQLResponse(result.data));
}
@Delete()
async handleApiDelete(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiCoreService.delete(request);
res.status(200).send(cleanGraphQLResponse(result.data));
}
@Post()
async handleApiPost(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiCoreService.createOne(request);
res.status(201).send(cleanGraphQLResponse(result.data));
}
@Patch()
async handleApiPatch(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiCoreService.update(request);
res.status(200).send(cleanGraphQLResponse(result.data));
}
// This endpoint is not documented in the OpenAPI schema.
// We keep it to avoid a breaking change since it initially used PUT instead of PATCH,
// and because the PUT verb is often used as a PATCH.
@Put()
async handleApiPut(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiCoreService.update(request);
res.status(200).send(cleanGraphQLResponse(result.data));
}
}

View File

@ -0,0 +1,60 @@
import {
Controller,
Get,
Delete,
Post,
Req,
Res,
Patch,
Put,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { RestApiMetadataService } from 'src/engine/api/rest/services/rest-api-metadata.service';
import { cleanGraphQLResponse } from 'src/engine/api/rest/utils/clean-graphql-response.utils';
@Controller('rest/metadata/*')
export class RestApiMetadataController {
constructor(
private readonly restApiMetadataService: RestApiMetadataService,
) {}
@Get()
async handleApiGet(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiMetadataService.get(request);
res.status(200).send(cleanGraphQLResponse(result.data));
}
@Delete()
async handleApiDelete(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiMetadataService.delete(request);
res.status(200).send(cleanGraphQLResponse(result.data));
}
@Post()
async handleApiPost(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiMetadataService.create(request);
res.status(201).send(cleanGraphQLResponse(result.data));
}
@Patch()
async handleApiPatch(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiMetadataService.update(request);
res.status(200).send(cleanGraphQLResponse(result.data));
}
// This endpoint is not documented in the OpenAPI schema.
// We keep it to avoid a breaking change since it initially used PUT instead of PATCH,
// and because the PUT verb is often used as a PATCH.
@Put()
async handleApiPut(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiMetadataService.update(request);
res.status(200).send(cleanGraphQLResponse(result.data));
}
}

View File

@ -0,0 +1,24 @@
import { BadRequestException } from '@nestjs/common';
import { BaseGraphQLError } from 'src/engine/utils/graphql-errors.util';
const formatMessage = (message: BaseGraphQLError) => {
if (message.extensions) {
return message.extensions.response.message || message.extensions.response;
}
return message.message;
};
export class RestApiException extends BadRequestException {
constructor(errors: BaseGraphQLError[]) {
super({
statusCode: 400,
message:
errors.length === 1
? formatMessage(errors[0])
: JSON.stringify(errors.map((error) => formatMessage(error))),
error: 'Bad Request',
});
}
}

View File

@ -1,39 +0,0 @@
import { Controller, Get, Delete, Post, Put, Req, Res } from '@nestjs/common';
import { Request, Response } from 'express';
import { ApiRestMetadataService } from 'src/engine/api/rest/metadata-rest.service';
import { cleanGraphQLResponse } from 'src/engine/api/rest/api-rest.controller.utils';
@Controller('rest/metadata/*')
export class ApiRestMetadataController {
constructor(private readonly apiRestService: ApiRestMetadataService) {}
@Get()
async handleApiGet(@Req() request: Request, @Res() res: Response) {
const result = await this.apiRestService.get(request);
res.send(cleanGraphQLResponse(result.data));
}
@Delete()
async handleApiDelete(@Req() request: Request, @Res() res: Response) {
const result = await this.apiRestService.delete(request);
res.send(cleanGraphQLResponse(result.data));
}
@Post()
async handleApiPost(@Req() request: Request, @Res() res: Response) {
const result = await this.apiRestService.create(request);
res.send(cleanGraphQLResponse(result.data));
}
@Put()
async handleApiPut(@Req() request: Request, @Res() res: Response) {
const result = await this.apiRestService.update(request);
res.send(cleanGraphQLResponse(result.data));
}
}

View File

@ -1,325 +0,0 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { ApiRestQueryBuilderFactory } from 'src/engine/api/rest/api-rest-query-builder/api-rest-query-builder.factory';
import { ApiRestQuery } from 'src/engine/api/rest/types/api-rest-query.type';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { parseMetadataPath } from 'src/engine/api/rest/api-rest-query-builder/utils/parse-path.utils';
import { capitalize } from 'src/utils/capitalize';
import { getServerUrl } from 'src/utils/get-server-url';
@Injectable()
export class ApiRestMetadataService {
constructor(
private readonly tokenService: TokenService,
private readonly environmentService: EnvironmentService,
private readonly apiRestQueryBuilderFactory: ApiRestQueryBuilderFactory,
private readonly httpService: HttpService,
) {}
async callMetadata(request, data: ApiRestQuery) {
const baseUrl = getServerUrl(
request,
this.environmentService.get('SERVER_URL'),
);
try {
return await this.httpService.axiosRef.post(`${baseUrl}/metadata`, data, {
headers: {
'Content-Type': 'application/json',
Authorization: request.headers.authorization,
},
});
} catch (err) {
return {
data: {
error: `${err}. Please check your query.`,
status: err.response.status,
},
};
}
}
async fetchMetadataInputFields(request, fieldName: string) {
const query = `
query {
__type(name: "${fieldName}") {
inputFields { name }
}
}
`;
const data: ApiRestQuery = {
query,
variables: {},
};
const { data: response } = await this.callMetadata(request, data);
const fields = response.data.__type.inputFields.map((field) => field.name);
return fields;
}
fetchMetadataFields(objectNamePlural: string) {
const fields = `
type
name
label
description
icon
isCustom
isActive
isSystem
isNullable
createdAt
updatedAt
fromRelationMetadata {
id
relationType
toObjectMetadata {
id
dataSourceId
nameSingular
namePlural
isSystem
}
toFieldMetadataId
}
toRelationMetadata {
id
relationType
fromObjectMetadata {
id
dataSourceId
nameSingular
namePlural
isSystem
}
fromFieldMetadataId
}
defaultValue
options
`;
switch (objectNamePlural) {
case 'objects':
return `
dataSourceId
nameSingular
namePlural
labelSingular
labelPlural
description
icon
isCustom
isActive
isSystem
createdAt
updatedAt
labelIdentifierFieldMetadataId
imageIdentifierFieldMetadataId
fields(paging: { first: 1000 }) {
edges {
node {
id
${fields}
}
}
}
`;
case 'fields':
return fields;
case 'relations':
return `
relationType
fromObjectMetadata {
id
dataSourceId
nameSingular
namePlural
isSystem
}
fromObjectMetadataId
toObjectMetadata {
id
dataSourceId
nameSingular
namePlural
isSystem
}
toObjectMetadataId
fromFieldMetadataId
toFieldMetadataId
`;
}
}
generateFindManyQuery(objectNameSingular: string, objectNamePlural: string) {
const fields = this.fetchMetadataFields(objectNamePlural);
return `
query FindMany${capitalize(objectNamePlural)}(
$filter: ${objectNameSingular}Filter,
) {
${objectNamePlural}(
filter: $filter,
paging: { first: 1000 }
) {
edges {
node {
id
${fields}
}
}
}
}
`;
}
generateFindOneQuery(objectNameSingular: string, objectNamePlural: string) {
const fields = this.fetchMetadataFields(objectNamePlural);
return `
query FindOne${capitalize(objectNameSingular)}(
$id: ID!,
) {
${objectNameSingular}(id: $id) {
id
${fields}
}
}
`;
}
async get(request) {
try {
await this.tokenService.validateToken(request);
const { objectNameSingular, objectNamePlural, id } =
parseMetadataPath(request);
const query = id
? this.generateFindOneQuery(objectNameSingular, objectNamePlural)
: this.generateFindManyQuery(objectNameSingular, objectNamePlural);
const data: ApiRestQuery = {
query,
variables: id ? { id } : request.body,
};
return await this.callMetadata(request, data);
} catch (err) {
return { data: { error: err, status: err.status } };
}
}
async create(request) {
try {
await this.tokenService.validateToken(request);
const { objectNameSingular: objectName } = parseMetadataPath(request);
const objectNameCapitalized = capitalize(objectName);
const fieldName = `Create${objectNameCapitalized}Input`;
const fields = await this.fetchMetadataInputFields(request, fieldName);
const query = `
mutation Create${objectNameCapitalized}($input: CreateOne${objectNameCapitalized}Input!) {
createOne${objectNameCapitalized}(input: $input) {
id
${fields.map((field) => field).join('\n')}
}
}
`;
const data: ApiRestQuery = {
query,
variables: {
input: {
[objectName]: request.body,
},
},
};
return await this.callMetadata(request, data);
} catch (err) {
return { data: { error: err, status: err.status } };
}
}
async update(request) {
try {
await this.tokenService.validateToken(request);
const { objectNameSingular: objectName, id } = parseMetadataPath(request);
const objectNameCapitalized = capitalize(objectName);
if (!id) {
throw new BadRequestException(
`update ${objectName} query invalid. Id missing. eg: /rest/metadata/${objectName}/0d4389ef-ea9c-4ae8-ada1-1cddc440fb56`,
);
}
const fieldName = `Update${objectNameCapitalized}Input`;
const fields = await this.fetchMetadataInputFields(request, fieldName);
const query = `
mutation Update${objectNameCapitalized}($input: UpdateOne${objectNameCapitalized}Input!) {
updateOne${objectNameCapitalized}(input: $input) {
id
${fields.map((field) => field).join('\n')}
}
}
`;
const data: ApiRestQuery = {
query,
variables: {
input: {
update: request.body,
id,
},
},
};
return await this.callMetadata(request, data);
} catch (err) {
return { data: { error: err, status: err.status } };
}
}
async delete(request) {
try {
await this.tokenService.validateToken(request);
const { objectNameSingular: objectName, id } = parseMetadataPath(request);
const objectNameCapitalized = capitalize(objectName);
if (!id) {
throw new BadRequestException(
`delete ${objectName} query invalid. Id missing. eg: /rest/metadata/${objectName}/0d4389ef-ea9c-4ae8-ada1-1cddc440fb56`,
);
}
const query = `
mutation Delete${objectNameCapitalized}($input: DeleteOne${objectNameCapitalized}Input!) {
deleteOne${objectNameCapitalized}(input: $input) {
id
}
}
`;
const data: ApiRestQuery = {
query,
variables: {
input: {
id,
},
},
};
return await this.callMetadata(request, data);
} catch (err) {
return { data: { error: err, status: err.status } };
}
}
}

View File

@ -0,0 +1,46 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CoreQueryBuilderFactory } from 'src/engine/api/rest/rest-api-core-query-builder/core-query-builder.factory';
import { DeleteQueryFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/delete-query.factory';
import { CreateOneQueryFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/create-one-query.factory';
import { CreateManyQueryFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/create-many-query.factory';
import { UpdateQueryFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/update-query.factory';
import { FindOneQueryFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/find-one-query.factory';
import { FindManyQueryFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/find-many-query.factory';
import { DeleteVariablesFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/delete-variables.factory';
import { CreateVariablesFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/create-variables.factory';
import { UpdateVariablesFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/update-variables.factory';
import { GetVariablesFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/get-variables.factory';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
describe('CoreQueryBuilderFactory', () => {
let service: CoreQueryBuilderFactory;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
CoreQueryBuilderFactory,
{ provide: DeleteQueryFactory, useValue: {} },
{ provide: CreateOneQueryFactory, useValue: {} },
{ provide: CreateManyQueryFactory, useValue: {} },
{ provide: UpdateQueryFactory, useValue: {} },
{ provide: FindOneQueryFactory, useValue: {} },
{ provide: FindManyQueryFactory, useValue: {} },
{ provide: DeleteVariablesFactory, useValue: {} },
{ provide: CreateVariablesFactory, useValue: {} },
{ provide: UpdateVariablesFactory, useValue: {} },
{ provide: GetVariablesFactory, useValue: {} },
{ provide: ObjectMetadataService, useValue: {} },
{ provide: TokenService, useValue: {} },
{ provide: EnvironmentService, useValue: {} },
],
}).compile();
service = module.get<CoreQueryBuilderFactory>(CoreQueryBuilderFactory);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -2,28 +2,31 @@ import { BadRequestException, Injectable } from '@nestjs/common';
import { Request } from 'express';
import { DeleteQueryFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/delete-query.factory';
import { DeleteQueryFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/delete-query.factory';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { CreateQueryFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/create-query.factory';
import { UpdateQueryFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/update-query.factory';
import { FindOneQueryFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/find-one-query.factory';
import { FindManyQueryFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/find-many-query.factory';
import { DeleteVariablesFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/delete-variables.factory';
import { CreateVariablesFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/create-variables.factory';
import { UpdateVariablesFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/update-variables.factory';
import { GetVariablesFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/get-variables.factory';
import { parsePath } from 'src/engine/api/rest/api-rest-query-builder/utils/parse-path.utils';
import { computeDepth } from 'src/engine/api/rest/api-rest-query-builder/utils/compute-depth.utils';
import { CreateOneQueryFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/create-one-query.factory';
import { UpdateQueryFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/update-query.factory';
import { FindOneQueryFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/find-one-query.factory';
import { FindManyQueryFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/find-many-query.factory';
import { DeleteVariablesFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/delete-variables.factory';
import { CreateVariablesFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/create-variables.factory';
import { UpdateVariablesFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/update-variables.factory';
import { GetVariablesFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/get-variables.factory';
import { parseCorePath } from 'src/engine/api/rest/rest-api-core-query-builder/utils/path-parsers/parse-core-path.utils';
import { computeDepth } from 'src/engine/api/rest/rest-api-core-query-builder/utils/compute-depth.utils';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ApiRestQuery } from 'src/engine/api/rest/types/api-rest-query.type';
import { Query } from 'src/engine/api/rest/types/query.type';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { CreateManyQueryFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/create-many-query.factory';
import { parseCoreBatchPath } from 'src/engine/api/rest/rest-api-core-query-builder/utils/path-parsers/parse-core-batch-path.utils';
@Injectable()
export class ApiRestQueryBuilderFactory {
export class CoreQueryBuilderFactory {
constructor(
private readonly deleteQueryFactory: DeleteQueryFactory,
private readonly createQueryFactory: CreateQueryFactory,
private readonly createOneQueryFactory: CreateOneQueryFactory,
private readonly createManyQueryFactory: CreateManyQueryFactory,
private readonly updateQueryFactory: UpdateQueryFactory,
private readonly findOneQueryFactory: FindOneQueryFactory,
private readonly findManyQueryFactory: FindManyQueryFactory,
@ -36,7 +39,10 @@ export class ApiRestQueryBuilderFactory {
private readonly environmentService: EnvironmentService,
) {}
async getObjectMetadata(request: Request): Promise<{
async getObjectMetadata(
request: Request,
parsedObject: string,
): Promise<{
objectMetadataItems: ObjectMetadataEntity[];
objectMetadataItem: ObjectMetadataEntity;
}> {
@ -53,8 +59,6 @@ export class ApiRestQueryBuilderFactory {
);
}
const { object: parsedObject } = parsePath(request);
const [objectMetadata] = objectMetadataItems.filter(
(object) => object.namePlural === parsedObject,
);
@ -81,10 +85,11 @@ export class ApiRestQueryBuilderFactory {
};
}
async delete(request: Request): Promise<ApiRestQuery> {
const objectMetadata = await this.getObjectMetadata(request);
async delete(request: Request): Promise<Query> {
const { object: parsedObject } = parseCorePath(request);
const objectMetadata = await this.getObjectMetadata(request, parsedObject);
const { id } = parsePath(request);
const { id } = parseCorePath(request);
if (!id) {
throw new BadRequestException(
@ -98,23 +103,36 @@ export class ApiRestQueryBuilderFactory {
};
}
async create(request: Request): Promise<ApiRestQuery> {
const objectMetadata = await this.getObjectMetadata(request);
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.createQueryFactory.create(objectMetadata, depth),
query: this.createOneQueryFactory.create(objectMetadata, depth),
variables: this.createVariablesFactory.create(request),
};
}
async update(request: Request): Promise<ApiRestQuery> {
const objectMetadata = await this.getObjectMetadata(request);
async createMany(request: Request): Promise<Query> {
const { object: parsedObject } = parseCoreBatchPath(request);
const objectMetadata = await this.getObjectMetadata(request, parsedObject);
const depth = computeDepth(request);
return {
query: this.createManyQueryFactory.create(objectMetadata, depth),
variables: this.createVariablesFactory.create(request),
};
}
async update(request: Request): Promise<Query> {
const { object: parsedObject } = parseCorePath(request);
const objectMetadata = await this.getObjectMetadata(request, parsedObject);
const depth = computeDepth(request);
const { id } = parsePath(request);
const { id } = parseCorePath(request);
if (!id) {
throw new BadRequestException(
@ -128,12 +146,13 @@ export class ApiRestQueryBuilderFactory {
};
}
async get(request: Request): Promise<ApiRestQuery> {
const objectMetadata = await this.getObjectMetadata(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 } = parsePath(request);
const { id } = parseCorePath(request);
return {
query: id

View File

@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { CoreQueryBuilderFactory } from 'src/engine/api/rest/rest-api-core-query-builder/core-query-builder.factory';
import { coreQueryBuilderFactories } from 'src/engine/api/rest/rest-api-core-query-builder/factories/factories';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
@Module({
imports: [ObjectMetadataModule, AuthModule],
providers: [...coreQueryBuilderFactories, CoreQueryBuilderFactory],
exports: [CoreQueryBuilderFactory],
})
export class CoreQueryBuilderModule {}

View File

@ -0,0 +1,33 @@
import { Injectable } from '@nestjs/common';
import { capitalize } from 'src/utils/capitalize';
import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/rest-api-core-query-builder/utils/map-field-metadata-to-graphql-query.utils';
@Injectable()
export class CreateManyQueryFactory {
create(objectMetadata, depth?: number): string {
const objectNamePlural = capitalize(
objectMetadata.objectMetadataItem.namePlural,
);
const objectNameSingular = capitalize(
objectMetadata.objectMetadataItem.nameSingular,
);
return `
mutation Create${objectNamePlural}($data: [${objectNameSingular}CreateInput!]) {
create${objectNamePlural}(data: $data) {
id
${objectMetadata.objectMetadataItem.fields
.map((field) =>
mapFieldMetadataToGraphqlQuery(
objectMetadata.objectMetadataItems,
field,
depth,
),
)
.join('\n')}
}
}
`;
}
}

View File

@ -1,10 +1,10 @@
import { Injectable } from '@nestjs/common';
import { capitalize } from 'src/utils/capitalize';
import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/api-rest-query-builder/utils/map-field-metadata-to-graphql-query.utils';
import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/rest-api-core-query-builder/utils/map-field-metadata-to-graphql-query.utils';
@Injectable()
export class CreateQueryFactory {
export class CreateOneQueryFactory {
create(objectMetadata, depth?: number): string {
const objectNameSingular = capitalize(
objectMetadata.objectMetadataItem.nameSingular,

View File

@ -0,0 +1,20 @@
import { Injectable } from '@nestjs/common';
import { Request } from 'express';
import { QueryVariables } from 'src/engine/api/rest/types/query-variables.type';
@Injectable()
export class CreateVariablesFactory {
create(request: Request): QueryVariables {
const data = Array.isArray(request.body)
? request.body.map((recordData) => {
return { position: 'first', ...recordData };
})
: { position: 'first', ...request.body };
return {
data,
};
}
}

View File

@ -0,0 +1,12 @@
import { Injectable } from '@nestjs/common';
import { QueryVariables } from 'src/engine/api/rest/types/query-variables.type';
@Injectable()
export class DeleteVariablesFactory {
create(id: string): QueryVariables {
return {
id: id,
};
}
}

View File

@ -0,0 +1,31 @@
import { DeleteQueryFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/delete-query.factory';
import { CreateOneQueryFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/create-one-query.factory';
import { UpdateQueryFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/update-query.factory';
import { FindOneQueryFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/find-one-query.factory';
import { FindManyQueryFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/find-many-query.factory';
import { DeleteVariablesFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/delete-variables.factory';
import { CreateVariablesFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/create-variables.factory';
import { UpdateVariablesFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/update-variables.factory';
import { GetVariablesFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/get-variables.factory';
import { LastCursorInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/last-cursor-input.factory';
import { LimitInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/limit-input.factory';
import { OrderByInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/order-by-input.factory';
import { FilterInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-input.factory';
import { CreateManyQueryFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/create-many-query.factory';
export const coreQueryBuilderFactories = [
DeleteQueryFactory,
CreateOneQueryFactory,
CreateManyQueryFactory,
UpdateQueryFactory,
FindOneQueryFactory,
FindManyQueryFactory,
DeleteVariablesFactory,
CreateVariablesFactory,
UpdateVariablesFactory,
GetVariablesFactory,
LastCursorInputFactory,
LimitInputFactory,
OrderByInputFactory,
FilterInputFactory,
];

View File

@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { capitalize } from 'src/utils/capitalize';
import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/api-rest-query-builder/utils/map-field-metadata-to-graphql-query.utils';
import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/rest-api-core-query-builder/utils/map-field-metadata-to-graphql-query.utils';
@Injectable()
export class FindManyQueryFactory {

View File

@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { capitalize } from 'src/utils/capitalize';
import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/api-rest-query-builder/utils/map-field-metadata-to-graphql-query.utils';
import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/rest-api-core-query-builder/utils/map-field-metadata-to-graphql-query.utils';
@Injectable()
export class FindOneQueryFactory {

View File

@ -2,11 +2,11 @@ import { Injectable } from '@nestjs/common';
import { Request } from 'express';
import { LastCursorInputFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/last-cursor-input.factory';
import { LimitInputFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/limit-input.factory';
import { OrderByInputFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/order-by-input.factory';
import { FilterInputFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-input.factory';
import { ApiRestQueryVariables } from 'src/engine/api/rest/types/api-rest-query-variables.type';
import { LastCursorInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/last-cursor-input.factory';
import { LimitInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/limit-input.factory';
import { OrderByInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/order-by-input.factory';
import { FilterInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-input.factory';
import { QueryVariables } from 'src/engine/api/rest/types/query-variables.type';
@Injectable()
export class GetVariablesFactory {
@ -21,7 +21,7 @@ export class GetVariablesFactory {
id: string | undefined,
request: Request,
objectMetadata,
): ApiRestQueryVariables {
): QueryVariables {
if (id) {
return { filter: { id: { eq: id } } };
}

View File

@ -1,7 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing';
import { objectMetadataItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock';
import { FilterInputFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-input.factory';
import { FilterInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-input.factory';
describe('FilterInputFactory', () => {
const objectMetadata = { objectMetadataItem: objectMetadataItemMock };

View File

@ -1,6 +1,6 @@
import { Test, TestingModule } from '@nestjs/testing';
import { LastCursorInputFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/last-cursor-input.factory';
import { LastCursorInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/last-cursor-input.factory';
describe('LastCursorInputFactory', () => {
let service: LastCursorInputFactory;

View File

@ -1,6 +1,6 @@
import { Test, TestingModule } from '@nestjs/testing';
import { LimitInputFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/limit-input.factory';
import { LimitInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/limit-input.factory';
describe('LimitInputFactory', () => {
let service: LimitInputFactory;

View File

@ -3,7 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing';
import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { objectMetadataItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock';
import { OrderByInputFactory } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/order-by-input.factory';
import { OrderByInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/order-by-input.factory';
describe('OrderByInputFactory', () => {
const objectMetadata = { objectMetadataItem: objectMetadataItemMock };

View File

@ -2,10 +2,10 @@ import { Injectable } from '@nestjs/common';
import { Request } from 'express';
import { addDefaultConjunctionIfMissing } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/add-default-conjunction.utils';
import { checkFilterQuery } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/check-filter-query.utils';
import { parseFilter } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/parse-filter.utils';
import { FieldValue } from 'src/engine/api/rest/types/api-rest-field-value.type';
import { addDefaultConjunctionIfMissing } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/add-default-conjunction.utils';
import { checkFilterQuery } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/check-filter-query.utils';
import { parseFilter } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/parse-filter.utils';
import { FieldValue } from 'src/engine/api/rest/types/field-value.type';
@Injectable()
export class FilterInputFactory {

View File

@ -1,4 +1,4 @@
import { addDefaultConjunctionIfMissing } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/add-default-conjunction.utils';
import { addDefaultConjunctionIfMissing } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/add-default-conjunction.utils';
describe('addDefaultConjunctionIfMissing', () => {
it('should add default conjunction if missing', () => {

View File

@ -1,4 +1,4 @@
import { checkFilterEnumValues } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/check-filter-enum-values';
import { checkFilterEnumValues } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/check-filter-enum-values';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import {
fieldSelectMock,

View File

@ -1,4 +1,4 @@
import { checkFilterQuery } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/check-filter-query.utils';
import { checkFilterQuery } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/check-filter-query.utils';
describe('checkFilterQuery', () => {
it('should check filter query', () => {

View File

@ -1,5 +1,5 @@
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { formatFieldValue } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/format-field-values.utils';
import { formatFieldValue } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/format-field-values.utils';
describe('formatFieldValue', () => {
it('should format fieldNumber value', () => {

View File

@ -1,4 +1,4 @@
import { parseBaseFilter } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/parse-base-filter.utils';
import { parseBaseFilter } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/parse-base-filter.utils';
describe('parseBaseFilter', () => {
it('should parse simple filter string test 1', () => {

View File

@ -1,4 +1,4 @@
import { parseFilterContent } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/parse-filter-content.utils';
import { parseFilterContent } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/parse-filter-content.utils';
describe('parseFilterContent', () => {
it('should parse query filter test 1', () => {

View File

@ -1,5 +1,5 @@
import { objectMetadataItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock';
import { parseFilter } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/parse-filter.utils';
import { parseFilter } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/parse-filter.utils';
describe('parseFilter', () => {
it('should parse string filter test 1', () => {

View File

@ -1,4 +1,4 @@
import { Conjunctions } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/parse-filter.utils';
import { Conjunctions } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/parse-filter.utils';
export const DEFAULT_CONJUNCTION = Conjunctions.and;

View File

@ -1,7 +1,7 @@
import { BadRequestException } from '@nestjs/common';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FieldValue } from 'src/engine/api/rest/types/api-rest-field-value.type';
import { FieldValue } from 'src/engine/api/rest/types/field-value.type';
export const formatFieldValue = (
value: string,

View File

@ -2,15 +2,13 @@ import { BadRequestException } from '@nestjs/common';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { parseFilterContent } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/parse-filter-content.utils';
import { parseBaseFilter } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/parse-base-filter.utils';
import {
checkFields,
getFieldType,
} from 'src/engine/api/rest/api-rest-query-builder/utils/fields.utils';
import { formatFieldValue } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/format-field-values.utils';
import { FieldValue } from 'src/engine/api/rest/types/api-rest-field-value.type';
import { checkFilterEnumValues } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/check-filter-enum-values';
import { parseFilterContent } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/parse-filter-content.utils';
import { parseBaseFilter } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/parse-base-filter.utils';
import { checkFields } from 'src/engine/api/rest/rest-api-core-query-builder/utils/check-fields.utils';
import { formatFieldValue } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/format-field-values.utils';
import { FieldValue } from 'src/engine/api/rest/types/field-value.type';
import { checkFilterEnumValues } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/check-filter-enum-values';
import { getFieldType } from 'src/engine/api/rest/rest-api-core-query-builder/utils/get-field-type.utils';
export enum Conjunctions {
or = 'or',

View File

@ -7,7 +7,7 @@ import {
RecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { checkFields } from 'src/engine/api/rest/api-rest-query-builder/utils/fields.utils';
import { checkFields } from 'src/engine/api/rest/rest-api-core-query-builder/utils/check-fields.utils';
export const DEFAULT_ORDER_DIRECTION = OrderByDirection.AscNullsFirst;

View File

@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { capitalize } from 'src/utils/capitalize';
import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/api-rest-query-builder/utils/map-field-metadata-to-graphql-query.utils';
import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/rest-api-core-query-builder/utils/map-field-metadata-to-graphql-query.utils';
@Injectable()
export class UpdateQueryFactory {

View File

@ -2,11 +2,11 @@ import { Injectable } from '@nestjs/common';
import { Request } from 'express';
import { ApiRestQueryVariables } from 'src/engine/api/rest/types/api-rest-query-variables.type';
import { QueryVariables } from 'src/engine/api/rest/types/query-variables.type';
@Injectable()
export class UpdateVariablesFactory {
create(id: string, request: Request): ApiRestQueryVariables {
create(id: string, request: Request): QueryVariables {
return {
id,
data: request.body,

View File

@ -0,0 +1,16 @@
import { objectMetadataItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock';
import { checkFields } from 'src/engine/api/rest/rest-api-core-query-builder/utils/check-fields.utils';
describe('checkFields', () => {
it('should check field types', () => {
expect(() =>
checkFields(objectMetadataItemMock, ['fieldNumber']),
).not.toThrow();
expect(() => checkFields(objectMetadataItemMock, ['wrongField'])).toThrow();
expect(() =>
checkFields(objectMetadataItemMock, ['fieldNumber', 'wrongField']),
).toThrow();
});
});

View File

@ -1,4 +1,4 @@
import { computeDepth } from 'src/engine/api/rest/api-rest-query-builder/utils/compute-depth.utils';
import { computeDepth } from 'src/engine/api/rest/rest-api-core-query-builder/utils/compute-depth.utils';
describe('computeDepth', () => {
it('should compute depth from query', () => {

View File

@ -0,0 +1,11 @@
import { objectMetadataItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { getFieldType } from 'src/engine/api/rest/rest-api-core-query-builder/utils/get-field-type.utils';
describe('getFieldType', () => {
it('should get field type', () => {
expect(getFieldType(objectMetadataItemMock, 'fieldNumber')).toEqual(
FieldMetadataType.NUMBER,
);
});
});

View File

@ -5,7 +5,7 @@ import {
fieldStringMock,
objectMetadataItemMock,
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/api-rest-query-builder/utils/map-field-metadata-to-graphql-query.utils';
import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/rest-api-core-query-builder/utils/map-field-metadata-to-graphql-query.utils';
describe('mapFieldMetadataToGraphqlQuery', () => {
it('should map properly', () => {

View File

@ -3,21 +3,9 @@ import { BadRequestException } from '@nestjs/common';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
export const getFieldType = (
objectMetadata: ObjectMetadataInterface,
fieldName: string,
): FieldMetadataType | undefined => {
for (const fieldMetdata of objectMetadata.fields) {
if (fieldName === fieldMetdata.name) {
return fieldMetdata.type;
}
}
};
export const checkFields = (
objectMetadata: ObjectMetadataInterface,
fieldNames: string[],

View File

@ -0,0 +1,10 @@
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export const getFieldType = (
objectMetadata: ObjectMetadataInterface,
fieldName: string,
): FieldMetadataType | undefined => {
return objectMetadata.fields.find((field) => field.name === fieldName)?.type;
};

View File

@ -0,0 +1,11 @@
import { parseCoreBatchPath } from 'src/engine/api/rest/rest-api-core-query-builder/utils/path-parsers/parse-core-batch-path.utils';
describe('parseCoreBatchPath', () => {
it('should parse object from request path', () => {
const request: any = { path: '/rest/batch/companies' };
expect(parseCoreBatchPath(request)).toEqual({
object: 'companies',
});
});
});

View File

@ -0,0 +1,29 @@
import { parseCorePath } from 'src/engine/api/rest/rest-api-core-query-builder/utils/path-parsers/parse-core-path.utils';
describe('parseCorePath', () => {
it('should parse object from request path', () => {
const request: any = { path: '/rest/companies/uuid' };
expect(parseCorePath(request)).toEqual({
object: 'companies',
id: 'uuid',
});
});
it('should parse object from request path', () => {
const request: any = { path: '/rest/companies' };
expect(parseCorePath(request)).toEqual({
object: 'companies',
id: undefined,
});
});
it('should throw for wrong request path', () => {
const request: any = { path: '/rest/companies/uuid/toto' };
expect(() => parseCorePath(request)).toThrow(
"Query path '/rest/companies/uuid/toto' invalid. Valid examples: /rest/companies/id or /rest/companies",
);
});
});

View File

@ -0,0 +1,39 @@
import { parseMetadataPath } from 'src/engine/api/rest/rest-api-core-query-builder/utils/path-parsers/parse-metadata-path.utils';
describe('parseMetadataPath', () => {
it('should parse object from request path with uuid', () => {
const request: any = { path: '/rest/metadata/fields/uuid' };
expect(parseMetadataPath(request)).toEqual({
objectNameSingular: 'field',
objectNamePlural: 'fields',
id: 'uuid',
});
});
it('should parse object from request path', () => {
const request: any = { path: '/rest/metadata/fields' };
expect(parseMetadataPath(request)).toEqual({
objectNameSingular: 'field',
objectNamePlural: 'fields',
id: undefined,
});
});
it('should throw for wrong request path', () => {
const request: any = { path: '/rest/metadata/INVALID' };
expect(() => parseMetadataPath(request)).toThrow(
'Query path \'/rest/metadata/INVALID\' invalid. Metadata path "INVALID" does not exist. Valid examples: /rest/metadata/fields or /rest/metadata/objects or /rest/metadata/relations',
);
});
it('should throw for wrong request path', () => {
const request: any = { path: '/rest/metadata/fields/uuid/toto' };
expect(() => parseMetadataPath(request)).toThrow(
"Query path '/rest/metadata/fields/uuid/toto' invalid. Valid examples: /rest/metadata/fields or /rest/metadata/objects/id",
);
});
});

View File

@ -0,0 +1,5 @@
import { Request } from 'express';
export const parseCoreBatchPath = (request: Request): { object: string } => {
return { object: request.path.replace('/rest/batch/', '') };
};

View File

@ -0,0 +1,21 @@
import { BadRequestException } from '@nestjs/common';
import { Request } from 'express';
export const parseCorePath = (
request: Request,
): { object: string; id?: string } => {
const queryAction = request.path.replace('/rest/', '').split('/');
if (queryAction.length > 2) {
throw new BadRequestException(
`Query path '${request.path}' invalid. Valid examples: /rest/companies/id or /rest/companies`,
);
}
if (queryAction.length === 1) {
return { object: queryAction[0] };
}
return { object: queryAction[0], id: queryAction[1] };
};

View File

@ -2,30 +2,12 @@ import { BadRequestException } from '@nestjs/common';
import { Request } from 'express';
export const parsePath = (
request: Request,
): { object: string; id?: string } => {
const queryAction = request.path.replace('/rest/', '').split('/');
if (queryAction.length > 2) {
throw new BadRequestException(
`Query path '${request.path}' invalid. Valid examples: /rest/companies/id or /rest/companies`,
);
}
if (queryAction.length === 1) {
return { object: queryAction[0] };
}
return { object: queryAction[0], id: queryAction[1] };
};
export const parseMetadataPath = (
request: Request,
): { objectNameSingular: string; objectNamePlural: string; id?: string } => {
const queryAction = request.path.replace('/rest/metadata/', '').split('/');
if (queryAction.length > 3 || queryAction.length === 0) {
if (queryAction.length >= 3 || queryAction.length === 0) {
throw new BadRequestException(
`Query path '${request.path}' invalid. Valid examples: /rest/metadata/fields or /rest/metadata/objects/id`,
);

View File

@ -0,0 +1,23 @@
import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { RestApiCoreController } from 'src/engine/api/rest/controllers/rest-api-core.controller';
import { RestApiCoreService } from 'src/engine/api/rest/services/rest-api-core.service';
import { CoreQueryBuilderModule } from 'src/engine/api/rest/rest-api-core-query-builder/core-query-builder.module';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { RestApiMetadataController } from 'src/engine/api/rest/controllers/rest-api-metadata.controller';
import { RestApiMetadataService } from 'src/engine/api/rest/services/rest-api-metadata.service';
import { RestApiCoreBatchController } from 'src/engine/api/rest/controllers/rest-api-core-batch.controller';
import { RestApiService } from 'src/engine/api/rest/services/rest-api.service';
@Module({
imports: [CoreQueryBuilderModule, AuthModule, HttpModule],
controllers: [
RestApiMetadataController,
RestApiCoreBatchController,
RestApiCoreController,
],
providers: [RestApiMetadataService, RestApiCoreService, RestApiService],
exports: [RestApiMetadataService],
})
export class RestApiModule {}

View File

@ -0,0 +1,30 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RestApiCoreService } from 'src/engine/api/rest/services/rest-api-core.service';
import { CoreQueryBuilderFactory } from 'src/engine/api/rest/rest-api-core-query-builder/core-query-builder.factory';
import { RestApiService } from 'src/engine/api/rest/services/rest-api.service';
describe('RestApiCoreService', () => {
let service: RestApiCoreService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
RestApiCoreService,
{
provide: CoreQueryBuilderFactory,
useValue: {},
},
{
provide: RestApiService,
useValue: {},
},
],
}).compile();
service = module.get<RestApiCoreService>(RestApiCoreService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,47 @@
import { Injectable } from '@nestjs/common';
import { Request } from 'express';
import { CoreQueryBuilderFactory } from 'src/engine/api/rest/rest-api-core-query-builder/core-query-builder.factory';
import {
GraphqlApiType,
RestApiService,
} from 'src/engine/api/rest/services/rest-api.service';
@Injectable()
export class RestApiCoreService {
constructor(
private readonly coreQueryBuilderFactory: CoreQueryBuilderFactory,
private readonly restApiService: RestApiService,
) {}
async get(request: Request) {
const data = await this.coreQueryBuilderFactory.get(request);
return await this.restApiService.call(GraphqlApiType.CORE, request, data);
}
async delete(request: Request) {
const data = await this.coreQueryBuilderFactory.delete(request);
return await this.restApiService.call(GraphqlApiType.CORE, request, data);
}
async createOne(request: Request) {
const data = await this.coreQueryBuilderFactory.createOne(request);
return await this.restApiService.call(GraphqlApiType.CORE, request, data);
}
async createMany(request: Request) {
const data = await this.coreQueryBuilderFactory.createMany(request);
return await this.restApiService.call(GraphqlApiType.CORE, request, data);
}
async update(request: Request) {
const data = await this.coreQueryBuilderFactory.update(request);
return await this.restApiService.call(GraphqlApiType.CORE, request, data);
}
}

View File

@ -0,0 +1,284 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { Query } from 'src/engine/api/rest/types/query.type';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { capitalize } from 'src/utils/capitalize';
import { parseMetadataPath } from 'src/engine/api/rest/rest-api-core-query-builder/utils/path-parsers/parse-metadata-path.utils';
import {
GraphqlApiType,
RestApiService,
} from 'src/engine/api/rest/services/rest-api.service';
@Injectable()
export class RestApiMetadataService {
constructor(
private readonly tokenService: TokenService,
private readonly restApiService: RestApiService,
) {}
fetchMetadataFields(objectNamePlural: string) {
const fields = `
type
name
label
description
icon
isCustom
isActive
isSystem
isNullable
createdAt
updatedAt
fromRelationMetadata {
id
relationType
toObjectMetadata {
id
dataSourceId
nameSingular
namePlural
isSystem
}
toFieldMetadataId
}
toRelationMetadata {
id
relationType
fromObjectMetadata {
id
dataSourceId
nameSingular
namePlural
isSystem
}
fromFieldMetadataId
}
defaultValue
options
`;
switch (objectNamePlural) {
case 'objects':
return `
dataSourceId
nameSingular
namePlural
labelSingular
labelPlural
description
icon
isCustom
isActive
isSystem
createdAt
updatedAt
labelIdentifierFieldMetadataId
imageIdentifierFieldMetadataId
fields(paging: { first: 1000 }) {
edges {
node {
id
${fields}
}
}
}
`;
case 'fields':
return fields;
case 'relations':
return `
relationType
fromObjectMetadata {
id
dataSourceId
nameSingular
namePlural
isSystem
}
fromObjectMetadataId
toObjectMetadata {
id
dataSourceId
nameSingular
namePlural
isSystem
}
toObjectMetadataId
fromFieldMetadataId
toFieldMetadataId
`;
}
}
generateFindManyQuery(objectNameSingular: string, objectNamePlural: string) {
const fields = this.fetchMetadataFields(objectNamePlural);
return `
query FindMany${capitalize(objectNamePlural)}(
$filter: ${objectNameSingular}Filter,
) {
${objectNamePlural}(
filter: $filter,
paging: { first: 1000 }
) {
edges {
node {
id
${fields}
}
}
}
}
`;
}
generateFindOneQuery(objectNameSingular: string, objectNamePlural: string) {
const fields = this.fetchMetadataFields(objectNamePlural);
return `
query FindOne${capitalize(objectNameSingular)}(
$id: UUID!,
) {
${objectNameSingular}(id: $id) {
id
${fields}
}
}
`;
}
async get(request) {
await this.tokenService.validateToken(request);
const { objectNameSingular, objectNamePlural, id } =
parseMetadataPath(request);
const query = id
? this.generateFindOneQuery(objectNameSingular, objectNamePlural)
: this.generateFindManyQuery(objectNameSingular, objectNamePlural);
const data: Query = {
query,
variables: id ? { id } : request.body,
};
return await this.restApiService.call(
GraphqlApiType.METADATA,
request,
data,
);
}
async create(request) {
await this.tokenService.validateToken(request);
const { objectNameSingular: objectName, objectNamePlural } =
parseMetadataPath(request);
const objectNameCapitalized = capitalize(objectName);
const fields = this.fetchMetadataFields(objectNamePlural);
const query = `
mutation Create${objectNameCapitalized}($input: CreateOne${objectNameCapitalized}Input!) {
createOne${objectNameCapitalized}(input: $input) {
id
${fields}
}
}
`;
const data: Query = {
query,
variables: {
input: {
[objectName]: request.body,
},
},
};
return await this.restApiService.call(
GraphqlApiType.METADATA,
request,
data,
);
}
async update(request) {
await this.tokenService.validateToken(request);
const {
objectNameSingular: objectName,
objectNamePlural,
id,
} = parseMetadataPath(request);
const objectNameCapitalized = capitalize(objectName);
if (!id) {
throw new BadRequestException(
`update ${objectName} query invalid. Id missing. eg: /rest/metadata/${objectName}/0d4389ef-ea9c-4ae8-ada1-1cddc440fb56`,
);
}
const fields = this.fetchMetadataFields(objectNamePlural);
const query = `
mutation Update${objectNameCapitalized}($input: UpdateOne${objectNameCapitalized}Input!) {
updateOne${objectNameCapitalized}(input: $input) {
id
${fields}
}
}
`;
const data: Query = {
query,
variables: {
input: {
update: request.body,
id,
},
},
};
return await this.restApiService.call(
GraphqlApiType.METADATA,
request,
data,
);
}
async delete(request) {
await this.tokenService.validateToken(request);
const { objectNameSingular: objectName, id } = parseMetadataPath(request);
const objectNameCapitalized = capitalize(objectName);
if (!id) {
throw new BadRequestException(
`delete ${objectName} query invalid. Id missing. eg: /rest/metadata/${objectName}/0d4389ef-ea9c-4ae8-ada1-1cddc440fb56`,
);
}
const query = `
mutation Delete${objectNameCapitalized}($input: DeleteOne${objectNameCapitalized}Input!) {
deleteOne${objectNameCapitalized}(input: $input) {
id
}
}
`;
const data: Query = {
query,
variables: {
input: {
id,
},
},
};
return await this.restApiService.call(
GraphqlApiType.METADATA,
request,
data,
);
}
}

View File

@ -0,0 +1,53 @@
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { Request } from 'express';
import { AxiosResponse } from 'axios';
import { Query } from 'src/engine/api/rest/types/query.type';
import { getServerUrl } from 'src/utils/get-server-url';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { RestApiException } from 'src/engine/api/rest/errors/RestApiException';
export enum GraphqlApiType {
CORE = 'core',
METADATA = 'metadata',
}
@Injectable()
export class RestApiService {
constructor(
private readonly environmentService: EnvironmentService,
private readonly httpService: HttpService,
) {}
async call(graphqlApiType: GraphqlApiType, request: Request, data: Query) {
const baseUrl = getServerUrl(
request,
this.environmentService.get('SERVER_URL'),
);
let response: AxiosResponse;
const url = `${baseUrl}/${
graphqlApiType === GraphqlApiType.CORE
? 'graphql'
: GraphqlApiType.METADATA
}`;
try {
response = await this.httpService.axiosRef.post(url, data, {
headers: {
'Content-Type': 'application/json',
Authorization: request.headers.authorization,
},
});
} catch (err) {
throw new RestApiException(err.response.data.errors);
}
if (response.data.errors?.length) {
throw new RestApiException(response.data.errors);
}
return response;
}
}

View File

@ -1,4 +0,0 @@
export type ApiRestQuery = {
query: string;
variables: object;
};

View File

@ -1,8 +1,9 @@
export type ApiRestQueryVariables = {
export type QueryVariables = {
id?: string;
data?: object | null;
filter?: object;
orderBy?: object;
limit?: number;
lastCursor?: string;
input?: object;
};

View File

@ -0,0 +1,6 @@
import { QueryVariables } from 'src/engine/api/rest/types/query-variables.type';
export type Query = {
query: string;
variables: QueryVariables;
};

View File

@ -1,4 +1,4 @@
import { cleanGraphQLResponse } from 'src/engine/api/rest/api-rest.controller.utils';
import { cleanGraphQLResponse } from 'src/engine/api/rest/utils/clean-graphql-response.utils';
describe('cleanGraphQLResponse', () => {
it('should remove edges/node from results', () => {