5582 get httpsapitwentycomrestmetadata objects filters dont work (#5906)

- Remove filters from metadata rest api
- add limite before and after parameters for metadata
- remove update from metadata relations
- fix typing issue
- fix naming
- fix before parameter

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
martmull
2024-06-18 18:55:13 +02:00
committed by GitHub
parent dbaa787d19
commit 6fd8dab552
87 changed files with 701 additions and 567 deletions
@@ -0,0 +1,24 @@
import { Injectable } from '@nestjs/common';
import { capitalize } from 'src/utils/capitalize';
import { fetchMetadataFields } from 'src/engine/api/rest/metadata/query-builder/utils/fetch-metadata-fields.utils';
@Injectable()
export class CreateMetadataQueryFactory {
create(objectNameSingular: string, objectNamePlural: string): string {
const objectNameCapitalized = capitalize(objectNameSingular);
const fields = fetchMetadataFields(objectNamePlural);
return `
mutation Create${objectNameCapitalized}($input: CreateOne${objectNameCapitalized}${
objectNameSingular === 'field' ? 'Metadata' : ''
}Input!) {
createOne${objectNameCapitalized}(input: $input) {
id
${fields}
}
}
`;
}
}
@@ -0,0 +1,18 @@
import { Injectable } from '@nestjs/common';
import { capitalize } from 'src/utils/capitalize';
@Injectable()
export class DeleteMetadataQueryFactory {
create(objectNameSingular: string): string {
const objectNameCapitalized = capitalize(objectNameSingular);
return `
mutation Delete${objectNameCapitalized}($input: DeleteOne${objectNameCapitalized}Input!) {
deleteOne${objectNameCapitalized}(input: $input) {
id
}
}
`;
}
}
@@ -0,0 +1,33 @@
import { Injectable } from '@nestjs/common';
import { capitalize } from 'src/utils/capitalize';
import { fetchMetadataFields } from 'src/engine/api/rest/metadata/query-builder/utils/fetch-metadata-fields.utils';
@Injectable()
export class FindManyMetadataQueryFactory {
create(objectNamePlural): string {
const fields = fetchMetadataFields(objectNamePlural);
return `
query FindMany${capitalize(objectNamePlural)}(
$paging: CursorPaging!
) {
${objectNamePlural}(
paging: $paging
) {
edges {
node {
id
${fields}
}
}
pageInfo {
hasNextPage
startCursor
endCursor
}
}
}
`;
}
}
@@ -0,0 +1,22 @@
import { Injectable } from '@nestjs/common';
import { capitalize } from 'src/utils/capitalize';
import { fetchMetadataFields } from 'src/engine/api/rest/metadata/query-builder/utils/fetch-metadata-fields.utils';
@Injectable()
export class FindOneMetadataQueryFactory {
create(objectNameSingular: string, objectNamePlural: string): string {
const fields = fetchMetadataFields(objectNamePlural);
return `
query FindOne${capitalize(objectNameSingular)}(
$id: UUID!,
) {
${objectNameSingular}(id: $id) {
id
${fields}
}
}
`;
}
}
@@ -0,0 +1,42 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { Request } from 'express';
import { LimitInputFactory } from 'src/engine/api/rest/input-factories/limit-input.factory';
import { EndingBeforeInputFactory } from 'src/engine/api/rest/input-factories/ending-before-input.factory';
import { StartingAfterInputFactory } from 'src/engine/api/rest/input-factories/starting-after-input.factory';
import { MetadataQueryVariables } from 'src/engine/api/rest/metadata/types/metadata-query-variables.type';
@Injectable()
export class GetMetadataVariablesFactory {
constructor(
private readonly startingAfterInputFactory: StartingAfterInputFactory,
private readonly endingBeforeInputFactory: EndingBeforeInputFactory,
private readonly limitInputFactory: LimitInputFactory,
) {}
create(id: string | undefined, request: Request): MetadataQueryVariables {
if (id) {
return { id };
}
const limit = this.limitInputFactory.create(request, 1000);
const before = this.endingBeforeInputFactory.create(request);
const after = this.startingAfterInputFactory.create(request);
if (before && after) {
throw new BadRequestException(
`Only one of 'endingBefore' and 'startingAfter' may be provided`,
);
}
return {
paging: {
first: !before ? limit : undefined,
last: before ? limit : undefined,
after,
before,
},
};
}
}
@@ -0,0 +1,17 @@
import { FindOneMetadataQueryFactory } from 'src/engine/api/rest/metadata/query-builder/factories/find-one-metadata-query.factory';
import { FindManyMetadataQueryFactory } from 'src/engine/api/rest/metadata/query-builder/factories/find-many-metadata-query.factory';
import { GetMetadataVariablesFactory } from 'src/engine/api/rest/metadata/query-builder/factories/get-metadata-variables.factory';
import { inputFactories } from 'src/engine/api/rest/input-factories/factories';
import { CreateMetadataQueryFactory } from 'src/engine/api/rest/metadata/query-builder/factories/create-metadata-query.factory';
import { UpdateMetadataQueryFactory } from 'src/engine/api/rest/metadata/query-builder/factories/update-metadata-query.factory';
import { DeleteMetadataQueryFactory } from 'src/engine/api/rest/metadata/query-builder/factories/delete-metadata-query.factory';
export const metadataQueryBuilderFactories = [
FindOneMetadataQueryFactory,
FindManyMetadataQueryFactory,
CreateMetadataQueryFactory,
DeleteMetadataQueryFactory,
UpdateMetadataQueryFactory,
GetMetadataVariablesFactory,
...inputFactories,
];
@@ -0,0 +1,24 @@
import { Injectable } from '@nestjs/common';
import { capitalize } from 'src/utils/capitalize';
import { fetchMetadataFields } from 'src/engine/api/rest/metadata/query-builder/utils/fetch-metadata-fields.utils';
@Injectable()
export class UpdateMetadataQueryFactory {
create(objectNameSingular: string, objectNamePlural: string): string {
const objectNameCapitalized = capitalize(objectNameSingular);
const fields = fetchMetadataFields(objectNamePlural);
return `
mutation Update${objectNameCapitalized}($input: UpdateOne${objectNameCapitalized}${
objectNameSingular === 'field' ? 'Metadata' : ''
}Input!) {
updateOne${objectNameCapitalized}(input: $input) {
id
${fields}
}
}
`;
}
}
@@ -0,0 +1,95 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { Request } from 'express';
import { GetMetadataVariablesFactory } from 'src/engine/api/rest/metadata/query-builder/factories/get-metadata-variables.factory';
import { FindOneMetadataQueryFactory } from 'src/engine/api/rest/metadata/query-builder/factories/find-one-metadata-query.factory';
import { FindManyMetadataQueryFactory } from 'src/engine/api/rest/metadata/query-builder/factories/find-many-metadata-query.factory';
import { parseMetadataPath } from 'src/engine/api/rest/metadata/query-builder/utils/parse-metadata-path.utils';
import { CreateMetadataQueryFactory } from 'src/engine/api/rest/metadata/query-builder/factories/create-metadata-query.factory';
import { UpdateMetadataQueryFactory } from 'src/engine/api/rest/metadata/query-builder/factories/update-metadata-query.factory';
import { DeleteMetadataQueryFactory } from 'src/engine/api/rest/metadata/query-builder/factories/delete-metadata-query.factory';
import { MetadataQuery } from 'src/engine/api/rest/metadata/types/metadata-query.type';
@Injectable()
export class MetadataQueryBuilderFactory {
constructor(
private readonly findOneQueryFactory: FindOneMetadataQueryFactory,
private readonly findManyQueryFactory: FindManyMetadataQueryFactory,
private readonly createQueryFactory: CreateMetadataQueryFactory,
private readonly updateQueryFactory: UpdateMetadataQueryFactory,
private readonly deleteQueryFactory: DeleteMetadataQueryFactory,
private readonly getMetadataVariablesFactory: GetMetadataVariablesFactory,
) {}
async get(request: Request): Promise<MetadataQuery> {
const { id, objectNameSingular, objectNamePlural } =
parseMetadataPath(request);
return {
query: id
? this.findOneQueryFactory.create(objectNameSingular, objectNamePlural)
: this.findManyQueryFactory.create(objectNamePlural),
variables: this.getMetadataVariablesFactory.create(id, request),
};
}
async create(request: Request): Promise<MetadataQuery> {
const { objectNameSingular, objectNamePlural } = parseMetadataPath(request);
return {
query: this.createQueryFactory.create(
objectNameSingular,
objectNamePlural,
),
variables: {
input: {
[objectNameSingular]: request.body,
},
},
};
}
async update(request: Request): Promise<MetadataQuery> {
const { objectNameSingular, objectNamePlural, id } =
parseMetadataPath(request);
if (!id) {
throw new BadRequestException(
`update ${objectNameSingular} query invalid. Id missing. eg: /rest/metadata/${objectNameSingular}/0d4389ef-ea9c-4ae8-ada1-1cddc440fb56`,
);
}
return {
query: this.updateQueryFactory.create(
objectNameSingular,
objectNamePlural,
),
variables: {
input: {
update: request.body,
id,
},
},
};
}
async delete(request: Request): Promise<MetadataQuery> {
const { objectNameSingular, id } = parseMetadataPath(request);
if (!id) {
throw new BadRequestException(
`delete ${objectNameSingular} query invalid. Id missing. eg: /rest/metadata/${objectNameSingular}/0d4389ef-ea9c-4ae8-ada1-1cddc440fb56`,
);
}
return {
query: this.deleteQueryFactory.create(objectNameSingular),
variables: {
input: {
id,
},
},
};
}
}
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { MetadataQueryBuilderFactory } from 'src/engine/api/rest/metadata/query-builder/metadata-query-builder.factory';
import { metadataQueryBuilderFactories } from 'src/engine/api/rest/metadata/query-builder/factories/metadata-factories';
@Module({
imports: [AuthModule],
providers: [...metadataQueryBuilderFactories, MetadataQueryBuilderFactory],
exports: [MetadataQueryBuilderFactory],
})
export class MetadataQueryBuilderModule {}
@@ -0,0 +1,39 @@
import { parseMetadataPath } from 'src/engine/api/rest/metadata/query-builder/utils/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",
);
});
});
@@ -0,0 +1,93 @@
export const 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
`;
}
};
@@ -0,0 +1,59 @@
import { BadRequestException } from '@nestjs/common';
import { Request } from 'express';
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: '' };
}
}
};
@@ -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/metadata/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.data));
}
@Delete()
async handleApiDelete(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiMetadataService.delete(request);
res.status(200).send(cleanGraphQLResponse(result.data.data));
}
@Post()
async handleApiPost(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiMetadataService.create(request);
res.status(201).send(cleanGraphQLResponse(result.data.data));
}
@Patch()
async handleApiPatch(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiMetadataService.update(request);
res.status(200).send(cleanGraphQLResponse(result.data.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.data));
}
}
@@ -0,0 +1,63 @@
import { Injectable } from '@nestjs/common';
import { Request } from 'express';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import {
GraphqlApiType,
RestApiService,
} from 'src/engine/api/rest/rest-api.service';
import { MetadataQueryBuilderFactory } from 'src/engine/api/rest/metadata/query-builder/metadata-query-builder.factory';
@Injectable()
export class RestApiMetadataService {
constructor(
private readonly tokenService: TokenService,
private readonly metadataQueryBuilderFactory: MetadataQueryBuilderFactory,
private readonly restApiService: RestApiService,
) {}
async get(request: Request) {
await this.tokenService.validateToken(request);
const data = await this.metadataQueryBuilderFactory.get(request);
return await this.restApiService.call(
GraphqlApiType.METADATA,
request,
data,
);
}
async create(request: Request) {
await this.tokenService.validateToken(request);
const data = await this.metadataQueryBuilderFactory.create(request);
return await this.restApiService.call(
GraphqlApiType.METADATA,
request,
data,
);
}
async update(request: Request) {
await this.tokenService.validateToken(request);
const data = await this.metadataQueryBuilderFactory.update(request);
return await this.restApiService.call(
GraphqlApiType.METADATA,
request,
data,
);
}
async delete(request: Request) {
await this.tokenService.validateToken(request);
const data = await this.metadataQueryBuilderFactory.delete(request);
return await this.restApiService.call(
GraphqlApiType.METADATA,
request,
data,
);
}
}
@@ -0,0 +1,10 @@
export type MetadataQueryVariables = {
id?: string;
input?: object;
paging?: {
first?: number;
last?: number;
after?: string;
before?: string;
};
};
@@ -0,0 +1,6 @@
import { MetadataQueryVariables } from 'src/engine/api/rest/metadata/types/metadata-query-variables.type';
export type MetadataQuery = {
query: string;
variables: MetadataQueryVariables;
};