feat: REST endpoints for metadata API (#3912)

* parse metadata path

* metadata rest api

* add queryAction condition and return object singular/plural

* handle GET endpoint for metadata

* FindOne and FindMany query for metadata endpoint

* Request all objects and nest fields in object request

---------

Co-authored-by: martmull <martmull@hotmail.fr>
This commit is contained in:
Aditya Pimpalkar
2024-02-20 14:17:41 +00:00
committed by GitHub
parent ec20117e80
commit 9d9ba97fb7
4 changed files with 414 additions and 2 deletions

View File

@ -19,3 +19,59 @@ export const parsePath = (
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) {
throw new BadRequestException(
`Query path '${request.path}' invalid. Valid examples: /rest/metadata/fields or /rest/metadata/objects/id`,
);
}
if (!['fields', 'objects', 'relations'].includes(queryAction[0])) {
throw new BadRequestException(
`Query path '${request.path}' invalid. Metadata path "${queryAction[0]}" does not exist. Valid examples: /rest/metadata/fields or /rest/metadata/objects or /rest/metadata/relations`,
);
}
const objectName = queryAction[0];
if (queryAction.length === 2) {
switch (objectName) {
case 'fields':
return {
objectNameSingular: 'field',
objectNamePlural: objectName,
id: queryAction[1],
};
case 'objects':
return {
objectNameSingular: 'object',
objectNamePlural: objectName,
id: queryAction[1],
};
case 'relations':
return {
objectNameSingular: 'relation',
objectNamePlural: objectName,
id: queryAction[1],
};
default:
return { objectNameSingular: '', objectNamePlural: '', id: '' };
}
} else {
switch (objectName) {
case 'fields':
return { objectNameSingular: 'field', objectNamePlural: objectName };
case 'objects':
return { objectNameSingular: 'object', objectNamePlural: objectName };
case 'relations':
return { objectNameSingular: 'relation', objectNamePlural: objectName };
default:
return { objectNameSingular: '', objectNamePlural: '' };
}
}
};

View File

@ -5,10 +5,12 @@ import { ApiRestController } from 'src/core/api-rest/api-rest.controller';
import { ApiRestService } from 'src/core/api-rest/api-rest.service';
import { ApiRestQueryBuilderModule } from 'src/core/api-rest/api-rest-query-builder/api-rest-query-builder.module';
import { AuthModule } from 'src/core/auth/auth.module';
import { ApiRestMetadataController } from 'src/core/api-rest/metadata-rest.controller';
import { ApiRestMetadataService } from 'src/core/api-rest/metadata-rest.service';
@Module({
imports: [ApiRestQueryBuilderModule, AuthModule, HttpModule],
controllers: [ApiRestController],
providers: [ApiRestService],
controllers: [ApiRestMetadataController, ApiRestController],
providers: [ApiRestMetadataService, ApiRestService],
})
export class ApiRestModule {}

View File

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

View File

@ -0,0 +1,323 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { ApiRestQueryBuilderFactory } from 'src/core/api-rest/api-rest-query-builder/api-rest-query-builder.factory';
import { ApiRestQuery } from 'src/core/api-rest/types/api-rest-query.type';
import { TokenService } from 'src/core/auth/services/token.service';
import { parseMetadataPath } from 'src/core/api-rest/api-rest-query-builder/utils/parse-path.utils';
import { capitalize } from 'src/utils/capitalize';
@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 =
this.environmentService.getServerUrl() ||
`${request.protocol}://${request.get('host')}`;
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 } };
}
}
}