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

@ -13,7 +13,7 @@ import { join } from 'path';
import { YogaDriverConfig, YogaDriver } from '@graphql-yoga/nestjs';
import { ApiRestModule } from 'src/engine/api/rest/api-rest.module';
import { RestApiModule } from 'src/engine/api/rest/rest-api.module';
import { ModulesModule } from 'src/modules/modules.module';
import { CoreGraphQLApiModule } from 'src/engine/api/graphql/core-graphql-api.module';
import { MetadataGraphQLApiModule } from 'src/engine/api/graphql/metadata-graphql-api.module';
@ -54,7 +54,7 @@ import { IntegrationsModule } from './engine/integrations/integrations.module';
// Api modules
CoreGraphQLApiModule,
MetadataGraphQLApiModule,
ApiRestModule,
RestApiModule,
// Conditional modules
...AppModule.getConditionalModules(),
],

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', () => {

View File

@ -7,10 +7,14 @@ import { TokenService } from 'src/engine/core-modules/auth/services/token.servic
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { baseSchema } from 'src/engine/core-modules/open-api/utils/base-schema.utils';
import {
computeBatchPath,
computeManyResultPath,
computeSingleResultPath,
} from 'src/engine/core-modules/open-api/utils/path.utils';
import { getErrorResponses } from 'src/engine/core-modules/open-api/utils/get-error-responses.utils';
import {
get400ErrorResponses,
get401ErrorResponses,
} from 'src/engine/core-modules/open-api/utils/get-error-responses.utils';
import {
computeMetadataSchemaComponents,
computeParameterComponents,
@ -21,8 +25,10 @@ import { computeWebhooks } from 'src/engine/core-modules/open-api/utils/computeW
import { capitalize } from 'src/utils/capitalize';
import {
getDeleteResponse200,
getManyResultResponse200,
getSingleResultSuccessResponse,
getFindManyResponse200,
getCreateOneResponse201,
getFindOneResponse200,
getUpdateOneResponse200,
} from 'src/engine/core-modules/open-api/utils/responses.utils';
import { getRequestBody } from 'src/engine/core-modules/open-api/utils/request-body.utils';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
@ -60,6 +66,7 @@ export class OpenApiService {
}
schema.paths = objectMetadataItems.reduce((paths, item) => {
paths[`/${item.namePlural}`] = computeManyResultPath(item);
paths[`/batch/${item.namePlural}`] = computeBatchPath(item);
paths[`/${item.namePlural}/{id}`] = computeSingleResultPath(item);
return paths;
@ -86,8 +93,8 @@ export class OpenApiService {
schemas: computeSchemaComponents(objectMetadataItems),
parameters: computeParameterComponents(),
responses: {
'400': getErrorResponses('Invalid request'),
'401': getErrorResponses('Unauthorized'),
'400': get400ErrorResponses(),
'401': get401ErrorResponses(),
},
};
@ -128,7 +135,7 @@ export class OpenApiService {
summary: `Find Many ${item.namePlural}`,
parameters: [{ $ref: '#/components/parameters/filter' }],
responses: {
'200': getManyResultResponse200(item),
'200': getFindManyResponse200(item),
'400': { $ref: '#/components/responses/400' },
'401': { $ref: '#/components/responses/401' },
},
@ -137,9 +144,9 @@ export class OpenApiService {
tags: [item.namePlural],
summary: `Create One ${item.nameSingular}`,
operationId: `createOne${capitalize(item.nameSingular)}`,
requestBody: getRequestBody(item),
requestBody: getRequestBody(capitalize(item.nameSingular)),
responses: {
'200': getSingleResultSuccessResponse(item),
'200': getCreateOneResponse201(item, true),
'400': { $ref: '#/components/responses/400' },
'401': { $ref: '#/components/responses/401' },
},
@ -151,7 +158,7 @@ export class OpenApiService {
summary: `Find One ${item.nameSingular}`,
parameters: [{ $ref: '#/components/parameters/idPath' }],
responses: {
'200': getSingleResultSuccessResponse(item),
'200': getFindOneResponse200(item),
'400': { $ref: '#/components/responses/400' },
'401': { $ref: '#/components/responses/401' },
},
@ -162,19 +169,19 @@ export class OpenApiService {
operationId: `deleteOne${capitalize(item.nameSingular)}`,
parameters: [{ $ref: '#/components/parameters/idPath' }],
responses: {
'200': getDeleteResponse200(item),
'200': getDeleteResponse200(item, true),
'400': { $ref: '#/components/responses/400' },
'401': { $ref: '#/components/responses/401' },
},
},
put: {
patch: {
tags: [item.namePlural],
summary: `Update One ${item.namePlural}`,
operationId: `updateOne${capitalize(item.nameSingular)}`,
parameters: [{ $ref: '#/components/parameters/idPath' }],
requestBody: getRequestBody(item),
requestBody: getRequestBody(capitalize(item.nameSingular)),
responses: {
'200': getSingleResultSuccessResponse(item),
'200': getUpdateOneResponse200(item, true),
'400': { $ref: '#/components/responses/400' },
'401': { $ref: '#/components/responses/401' },
},
@ -189,8 +196,8 @@ export class OpenApiService {
schemas: computeMetadataSchemaComponents(metadata),
parameters: computeParameterComponents(),
responses: {
'400': getErrorResponses('Invalid request'),
'401': getErrorResponses('Unauthorized'),
'400': get400ErrorResponses(),
'401': get401ErrorResponses(),
},
};

View File

@ -40,6 +40,17 @@ describe('computeSchemaComponents', () => {
},
},
},
ObjectsName: {
description: 'A list of objectsName',
example: {
fieldNumber: '',
},
items: {
$ref: '#/components/schemas/ObjectName',
},
required: ['fieldNumber'],
type: 'array',
},
});
});
});

View File

@ -8,10 +8,10 @@ import {
computeLimitParameters,
computeOrderByParameters,
} from 'src/engine/core-modules/open-api/utils/parameters.utils';
import { DEFAULT_ORDER_DIRECTION } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/order-by-input.factory';
import { FilterComparators } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/parse-base-filter.utils';
import { Conjunctions } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/parse-filter.utils';
import { DEFAULT_CONJUNCTION } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/add-default-conjunction.utils';
import { DEFAULT_ORDER_DIRECTION } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/order-by-input.factory';
import { FilterComparators } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/parse-base-filter.utils';
import { Conjunctions } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/parse-filter.utils';
import { DEFAULT_CONJUNCTION } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/add-default-conjunction.utils';
describe('computeParameters', () => {
describe('computeLimit', () => {

View File

@ -105,6 +105,33 @@ const getRequiredFields = (item: ObjectMetadataEntity): string[] => {
}, [] as string[]);
};
const computeBatchSchemaComponent = (
item: ObjectMetadataEntity,
): OpenAPIV3_1.SchemaObject => {
const result = {
type: 'array',
description: `A list of ${item.namePlural}`,
items: { $ref: `#/components/schemas/${capitalize(item.nameSingular)}` },
example: [{}],
} as OpenAPIV3_1.SchemaObject;
const requiredFields = getRequiredFields(item);
if (requiredFields?.length) {
result.required = requiredFields;
result.example = requiredFields.reduce(
(example, requiredField) => {
example[requiredField] = '';
return example;
},
{} as Record<string, string>,
);
}
return result;
};
const computeSchemaComponent = (
item: ObjectMetadataEntity,
): OpenAPIV3_1.SchemaObject => {
@ -138,6 +165,7 @@ export const computeSchemaComponents = (
return objectMetadataItems.reduce(
(schemas, item) => {
schemas[capitalize(item.nameSingular)] = computeSchemaComponent(item);
schemas[capitalize(item.namePlural)] = computeBatchSchemaComponent(item);
return schemas;
},
@ -168,6 +196,7 @@ export const computeMetadataSchemaComponents = (
case 'object': {
schemas[`${capitalize(item.nameSingular)}`] = {
type: 'object',
description: `An object`,
properties: {
dataSourceId: { type: 'string' },
nameSingular: { type: 'string' },
@ -201,6 +230,15 @@ export const computeMetadataSchemaComponents = (
},
},
},
example: {},
};
schemas[`${capitalize(item.namePlural)}`] = {
type: 'array',
description: `A list of ${item.namePlural}`,
items: {
$ref: `#/components/schemas/${capitalize(item.nameSingular)}`,
},
example: [{}],
};
return schemas;
@ -208,6 +246,7 @@ export const computeMetadataSchemaComponents = (
case 'field': {
schemas[`${capitalize(item.nameSingular)}`] = {
type: 'object',
description: `A field`,
properties: {
type: { type: 'string' },
name: { type: 'string' },
@ -259,6 +298,15 @@ export const computeMetadataSchemaComponents = (
defaultValue: { type: 'object' },
options: { type: 'object' },
},
example: {},
};
schemas[`${capitalize(item.namePlural)}`] = {
type: 'array',
description: `A list of ${item.namePlural}`,
items: {
$ref: `#/components/schemas/${capitalize(item.nameSingular)}`,
},
example: [{}],
};
return schemas;
@ -266,6 +314,7 @@ export const computeMetadataSchemaComponents = (
case 'relation': {
schemas[`${capitalize(item.nameSingular)}`] = {
type: 'object',
description: 'A relation',
properties: {
relationType: { type: 'string' },
fromObjectMetadata: {
@ -293,6 +342,15 @@ export const computeMetadataSchemaComponents = (
fromFieldMetadataId: { type: 'string' },
toFieldMetadataId: { type: 'string' },
},
example: {},
};
schemas[`${capitalize(item.namePlural)}`] = {
type: 'array',
description: `A list of ${item.namePlural}`,
items: {
$ref: `#/components/schemas/${capitalize(item.nameSingular)}`,
},
example: [{}],
};
}
}

View File

@ -1,17 +1,45 @@
import { OpenAPIV3_1 } from 'openapi-types';
export const getErrorResponses = (
description: string,
): OpenAPIV3_1.ResponseObject => {
export const get400ErrorResponses = (): OpenAPIV3_1.ResponseObject => {
return {
description,
description: 'Bad Request',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
statusCode: { type: 'number' },
message: { type: 'string' },
error: { type: 'string' },
},
example: {
statusCode: 400,
message: 'error message',
error: 'Bad Request',
},
},
},
},
};
};
export const get401ErrorResponses = (): OpenAPIV3_1.ResponseObject => {
return {
description: 'Unauthorized',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
statusCode: { type: 'number' },
message: { type: 'string' },
error: { type: 'string' },
},
example: {
statusCode: 401,
message: 'Token invalid.',
error: 'Unauthorized',
},
},
},
},

View File

@ -2,10 +2,10 @@ import { OpenAPIV3_1 } from 'openapi-types';
import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { FilterComparators } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/parse-base-filter.utils';
import { Conjunctions } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/parse-filter.utils';
import { DEFAULT_ORDER_DIRECTION } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/order-by-input.factory';
import { DEFAULT_CONJUNCTION } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/add-default-conjunction.utils';
import { FilterComparators } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/parse-base-filter.utils';
import { Conjunctions } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/parse-filter.utils';
import { DEFAULT_ORDER_DIRECTION } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/order-by-input.factory';
import { DEFAULT_CONJUNCTION } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/add-default-conjunction.utils';
export const computeLimitParameters = (): OpenAPIV3_1.ParameterObject => {
return {

View File

@ -5,11 +5,33 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
import {
getDeleteResponse200,
getJsonResponse,
getManyResultResponse200,
getSingleResultSuccessResponse,
getFindManyResponse200,
getCreateOneResponse201,
getCreateManyResponse201,
getFindOneResponse200,
getUpdateOneResponse200,
} from 'src/engine/core-modules/open-api/utils/responses.utils';
import { getRequestBody } from 'src/engine/core-modules/open-api/utils/request-body.utils';
export const computeBatchPath = (
item: ObjectMetadataEntity,
): OpenAPIV3_1.PathItemObject => {
return {
post: {
tags: [item.namePlural],
summary: `Create Many ${item.namePlural}`,
operationId: `createMany${capitalize(item.namePlural)}`,
parameters: [{ $ref: '#/components/parameters/depth' }],
requestBody: getRequestBody(capitalize(item.namePlural)),
responses: {
'201': getCreateManyResponse201(item),
'400': { $ref: '#/components/responses/400' },
'401': { $ref: '#/components/responses/401' },
},
},
} as OpenAPIV3_1.PathItemObject;
};
export const computeManyResultPath = (
item: ObjectMetadataEntity,
): OpenAPIV3_1.PathItemObject => {
@ -27,7 +49,7 @@ export const computeManyResultPath = (
{ $ref: '#/components/parameters/lastCursor' },
],
responses: {
'200': getManyResultResponse200(item),
'200': getFindManyResponse200(item),
'400': { $ref: '#/components/responses/400' },
'401': { $ref: '#/components/responses/401' },
},
@ -37,9 +59,9 @@ export const computeManyResultPath = (
summary: `Create One ${item.nameSingular}`,
operationId: `createOne${capitalize(item.nameSingular)}`,
parameters: [{ $ref: '#/components/parameters/depth' }],
requestBody: getRequestBody(item),
requestBody: getRequestBody(capitalize(item.nameSingular)),
responses: {
'201': getSingleResultSuccessResponse(item),
'201': getCreateOneResponse201(item),
'400': { $ref: '#/components/responses/400' },
'401': { $ref: '#/components/responses/401' },
},
@ -61,7 +83,7 @@ export const computeSingleResultPath = (
{ $ref: '#/components/parameters/depth' },
],
responses: {
'200': getSingleResultSuccessResponse(item),
'200': getFindOneResponse200(item),
'400': { $ref: '#/components/responses/400' },
'401': { $ref: '#/components/responses/401' },
},
@ -77,7 +99,7 @@ export const computeSingleResultPath = (
'401': { $ref: '#/components/responses/401' },
},
},
put: {
patch: {
tags: [item.namePlural],
summary: `Update One ${item.namePlural}`,
operationId: `UpdateOne${capitalize(item.nameSingular)}`,
@ -85,9 +107,9 @@ export const computeSingleResultPath = (
{ $ref: '#/components/parameters/idPath' },
{ $ref: '#/components/parameters/depth' },
],
requestBody: getRequestBody(item),
requestBody: getRequestBody(capitalize(item.nameSingular)),
responses: {
'200': getSingleResultSuccessResponse(item),
'200': getUpdateOneResponse200(item),
'400': { $ref: '#/components/responses/400' },
'401': { $ref: '#/components/responses/401' },
},

View File

@ -1,16 +1,11 @@
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { capitalize } from 'src/utils/capitalize';
export const getRequestBody = (
item: Pick<ObjectMetadataEntity, 'nameSingular'>,
) => {
export const getRequestBody = (name: string) => {
return {
description: 'body',
required: true,
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/${capitalize(item.nameSingular)}`,
$ref: `#/components/schemas/${name}`,
},
},
},

View File

@ -1,7 +1,7 @@
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { capitalize } from 'src/utils/capitalize';
export const getManyResultResponse200 = (
export const getFindManyResponse200 = (
item: Pick<ObjectMetadataEntity, 'nameSingular' | 'namePlural'>,
) => {
return {
@ -28,7 +28,8 @@ export const getManyResultResponse200 = (
example: {
data: {
[item.namePlural]: [
`${capitalize(item.nameSingular)}Object`,
`${capitalize(item.nameSingular)}Object1`,
`${capitalize(item.nameSingular)}Object2`,
'...',
],
},
@ -39,7 +40,7 @@ export const getManyResultResponse200 = (
};
};
export const getSingleResultSuccessResponse = (
export const getFindOneResponse200 = (
item: Pick<ObjectMetadataEntity, 'nameSingular'>,
) => {
return {
@ -58,14 +59,54 @@ export const getSingleResultSuccessResponse = (
},
},
},
example: {
data: {
[item.nameSingular]: `${capitalize(item.nameSingular)}Object`,
},
},
},
},
},
};
};
export const getDeleteResponse200 = (
export const getCreateOneResponse201 = (
item: Pick<ObjectMetadataEntity, 'nameSingular'>,
fromMetadata = false,
) => {
const one = fromMetadata ? 'One' : '';
return {
description: 'Successful operation',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
data: {
type: 'object',
properties: {
[`create${one}${capitalize(item.nameSingular)}`]: {
$ref: `#/components/schemas/${capitalize(item.nameSingular)}`,
},
},
},
},
example: {
data: {
[`create${one}${capitalize(item.nameSingular)}`]: `${capitalize(
item.nameSingular,
)}Object`,
},
},
},
},
},
};
};
export const getCreateManyResponse201 = (
item: Pick<ObjectMetadataEntity, 'nameSingular' | 'namePlural'>,
) => {
return {
description: 'Successful operation',
@ -77,7 +118,84 @@ export const getDeleteResponse200 = (
data: {
type: 'object',
properties: {
[item.nameSingular]: {
[`create${capitalize(item.namePlural)}`]: {
type: 'array',
items: {
$ref: `#/components/schemas/${capitalize(
item.nameSingular,
)}`,
},
},
},
},
},
example: {
data: {
[`create${capitalize(item.namePlural)}`]: [
`${capitalize(item.nameSingular)}Object1`,
`${capitalize(item.nameSingular)}Object2`,
'...',
],
},
},
},
},
},
};
};
export const getUpdateOneResponse200 = (
item: Pick<ObjectMetadataEntity, 'nameSingular'>,
fromMetadata = false,
) => {
const one = fromMetadata ? 'One' : '';
return {
description: 'Successful operation',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
data: {
type: 'object',
properties: {
[`update${one}${capitalize(item.nameSingular)}`]: {
$ref: `#/components/schemas/${capitalize(item.nameSingular)}`,
},
},
},
},
example: {
data: {
[`update${one}${capitalize(item.nameSingular)}`]: `${capitalize(
item.nameSingular,
)}Object`,
},
},
},
},
},
};
};
export const getDeleteResponse200 = (
item: Pick<ObjectMetadataEntity, 'nameSingular'>,
fromMetadata = false,
) => {
const one = fromMetadata ? 'One' : '';
return {
description: 'Successful operation',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
data: {
type: 'object',
properties: {
[`delete${one}${capitalize(item.nameSingular)}`]: {
type: 'object',
properties: {
id: {
@ -89,6 +207,13 @@ export const getDeleteResponse200 = (
},
},
},
example: {
data: {
[`delete${one}${capitalize(item.nameSingular)}`]: {
id: 'ffe75ac3-9786-4846-b56f-640685c3631e',
},
},
},
},
},
},

View File

@ -8,6 +8,8 @@ import bytes from 'bytes';
import { useContainer } from 'class-validator';
import '@sentry/tracing';
import { ApplyCorsToExceptions } from 'src/utils/apply-cors-to-exceptions';
import { AppModule } from './app.module';
import { generateFrontConfig } from './utils/generate-front-config';
@ -38,6 +40,8 @@ const bootstrap = async () => {
app.use(Sentry.Handlers.tracingHandler());
}
app.useGlobalFilters(new ApplyCorsToExceptions());
// Apply validation pipes globally
app.useGlobalPipes(
new ValidationPipe({

View File

@ -0,0 +1,26 @@
import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common';
import { Response } from 'express';
// In case of exception in middleware run before the CORS middleware (eg: JSON Middleware that checks the request body),
// the CORS headers are missing in the response.
// This class add CORS headers to exception response to avoid misleading CORS error
@Catch()
export class ApplyCorsToExceptions implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
response.header('Access-Control-Allow-Origin', '*');
response.header(
'Access-Control-Allow-Methods',
'GET,HEAD,PUT,PATCH,POST,DELETE',
);
response.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept',
);
response.status(exception.getStatus()).json(exception.response);
}
}