feat(ai): add mcp-metadata (#13150)
Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
@ -0,0 +1,25 @@
|
||||
import { Controller, Post, Req, UseGuards } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
|
||||
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { MCPMetadataService } from 'src/engine/api/mcp/services/mcp-metadata.service';
|
||||
|
||||
@Controller('mcp/metadata')
|
||||
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
|
||||
export class McpMetadataController {
|
||||
constructor(private readonly mCPMetadataService: MCPMetadataService) {}
|
||||
|
||||
@Post()
|
||||
async getMcpMetadata(
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@Req() request: Request,
|
||||
) {
|
||||
return await this.mCPMetadataService.handleMCPQuery(request, {
|
||||
workspace,
|
||||
});
|
||||
}
|
||||
}
|
||||
42
packages/twenty-server/src/engine/api/mcp/mcp.module.ts
Normal file
42
packages/twenty-server/src/engine/api/mcp/mcp.module.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AiModule } from 'src/engine/core-modules/ai/ai.module';
|
||||
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
|
||||
import { McpMetadataController } from 'src/engine/api/mcp/controllers/mcp-metadata.controller';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
import { MCPMetadataService } from 'src/engine/api/mcp/services/mcp-metadata.service';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { RestApiModule } from 'src/engine/api/rest/rest-api.module';
|
||||
import { MetadataQueryBuilderModule } from 'src/engine/api/rest/metadata/query-builder/metadata-query-builder.module';
|
||||
import { MCPMetadataToolsService } from 'src/engine/api/mcp/services/tools/mcp-metadata-tools.service';
|
||||
import { UpdateToolsService } from 'src/engine/api/mcp/services/tools/update.tools.service';
|
||||
import { CreateToolsService } from 'src/engine/api/mcp/services/tools/create.tools.service';
|
||||
import { DeleteToolsService } from 'src/engine/api/mcp/services/tools/delete.tools.service';
|
||||
import { GetToolsService } from 'src/engine/api/mcp/services/tools/get.tools.service';
|
||||
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([RoleEntity], 'core'),
|
||||
AiModule,
|
||||
TokenModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
FeatureFlagModule,
|
||||
RestApiModule,
|
||||
MetadataQueryBuilderModule,
|
||||
MetricsModule,
|
||||
],
|
||||
controllers: [McpMetadataController],
|
||||
exports: [],
|
||||
providers: [
|
||||
MCPMetadataService,
|
||||
MCPMetadataToolsService,
|
||||
CreateToolsService,
|
||||
UpdateToolsService,
|
||||
DeleteToolsService,
|
||||
GetToolsService,
|
||||
],
|
||||
})
|
||||
export class McpModule {}
|
||||
@ -0,0 +1,220 @@
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
import { validationMetadatasToSchemas } from 'class-validator-jsonschema';
|
||||
import { JSONSchema7 } from 'json-schema';
|
||||
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { wrapJsonRpcResponse } from 'src/engine/core-modules/ai/utils/wrap-jsonrpc-response.util';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { CreateToolsService } from 'src/engine/api/mcp/services/tools/create.tools.service';
|
||||
import { UpdateToolsService } from 'src/engine/api/mcp/services/tools/update.tools.service';
|
||||
import { DeleteToolsService } from 'src/engine/api/mcp/services/tools/delete.tools.service';
|
||||
import { GetToolsService } from 'src/engine/api/mcp/services/tools/get.tools.service';
|
||||
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
|
||||
|
||||
@Injectable()
|
||||
export class MCPMetadataService {
|
||||
schemas: Record<string, JSONSchema7>;
|
||||
|
||||
constructor(
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly createToolsService: CreateToolsService,
|
||||
private readonly updateToolsService: UpdateToolsService,
|
||||
private readonly deleteToolsService: DeleteToolsService,
|
||||
private readonly getToolsService: GetToolsService,
|
||||
private readonly metricsService: MetricsService,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
this.schemas = validationMetadatasToSchemas() as Record<
|
||||
string,
|
||||
JSONSchema7
|
||||
>;
|
||||
}
|
||||
|
||||
async checkAiEnabled(workspaceId: string): Promise<void> {
|
||||
const isAiEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_AI_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!isAiEnabled) {
|
||||
throw new HttpException(
|
||||
'AI feature is not enabled for this workspace',
|
||||
HttpStatus.FORBIDDEN,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
handleInitialize(requestId: string | number | null) {
|
||||
return wrapJsonRpcResponse(requestId, {
|
||||
result: {
|
||||
capabilities: {
|
||||
tools: { listChanged: false },
|
||||
resources: { listChanged: false },
|
||||
prompts: { listChanged: false },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
get commonProperties() {
|
||||
return {
|
||||
fields: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Names of field properties to include in the response for field entities. ',
|
||||
examples: [
|
||||
'type',
|
||||
'name',
|
||||
'label',
|
||||
'description',
|
||||
'icon',
|
||||
'isCustom',
|
||||
'isActive',
|
||||
'isSystem',
|
||||
'isNullable',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'defaultValue',
|
||||
'options',
|
||||
'relation',
|
||||
],
|
||||
},
|
||||
description:
|
||||
'List of field names to select in the query for field entity. Strongly recommended to limit token usage and reduce response size. Use this to include only the properties you need.',
|
||||
},
|
||||
objects: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Object property names to include in the response for object entities.',
|
||||
examples: [
|
||||
'dataSourceId',
|
||||
'nameSingular',
|
||||
'namePlural',
|
||||
'labelSingular',
|
||||
'labelPlural',
|
||||
'description',
|
||||
'icon',
|
||||
'isCustom',
|
||||
'isActive',
|
||||
'isSystem',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'labelIdentifierFieldMetadataId',
|
||||
'imageIdentifierFieldMetadataId',
|
||||
],
|
||||
},
|
||||
description:
|
||||
'List of object properties to select in the query for object entities. Strongly recommended to limit token usage and reduce response size. Specify only the necessary properties to optimize your request.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
get tools() {
|
||||
return [
|
||||
...this.createToolsService.tools,
|
||||
...this.updateToolsService.tools,
|
||||
...this.deleteToolsService.tools,
|
||||
...this.getToolsService.tools,
|
||||
];
|
||||
}
|
||||
|
||||
async handleToolCall(
|
||||
request: Request,
|
||||
): Promise<Parameters<typeof wrapJsonRpcResponse>[1]> {
|
||||
const tool = this.tools.find(
|
||||
({ name }) => name === request.body.params.name,
|
||||
);
|
||||
|
||||
if (tool) {
|
||||
try {
|
||||
const result = await tool.execute(request);
|
||||
|
||||
await this.metricsService.incrementCounter({
|
||||
key: MetricsKeys.AIToolExecutionSucceeded,
|
||||
attributes: {
|
||||
tool: request.body.params.name,
|
||||
},
|
||||
});
|
||||
|
||||
return { result };
|
||||
} catch (err) {
|
||||
await this.metricsService.incrementCounter({
|
||||
key: MetricsKeys.AIToolExecutionFailed,
|
||||
attributes: {
|
||||
tool: request.body.params.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
error: {
|
||||
code: HttpStatus.NOT_FOUND,
|
||||
message: `Tool ${request.body.params.name} not found`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async listTools(request: Request) {
|
||||
return wrapJsonRpcResponse(request.body.id, {
|
||||
result: {
|
||||
capabilities: {
|
||||
tools: { listChanged: false },
|
||||
},
|
||||
commonProperties: this.commonProperties,
|
||||
tools: Object.values(this.tools),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async handleMCPQuery(
|
||||
request: Request,
|
||||
{
|
||||
workspace,
|
||||
}: { workspace: Workspace; userWorkspaceId?: string; apiKey?: string },
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
await this.checkAiEnabled(workspace.id);
|
||||
|
||||
if (request.body.method === 'initialize') {
|
||||
return this.handleInitialize(request.body.id);
|
||||
}
|
||||
|
||||
if (request.body.method === 'ping') {
|
||||
return wrapJsonRpcResponse(
|
||||
request.body.id,
|
||||
{
|
||||
result: {},
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
if (request.body.method === 'tools/call' && request.body.params) {
|
||||
return wrapJsonRpcResponse(
|
||||
request.body.id,
|
||||
await this.handleToolCall(request),
|
||||
);
|
||||
}
|
||||
|
||||
return this.listTools(request);
|
||||
} catch (error) {
|
||||
return wrapJsonRpcResponse(request.body.id, {
|
||||
error: {
|
||||
code: error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
message:
|
||||
error.response?.messages?.join?.('\n') || 'Failed to execute tool',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
import pick from 'lodash.pick';
|
||||
|
||||
import { MetadataQueryBuilderFactory } from 'src/engine/api/rest/metadata/query-builder/metadata-query-builder.factory';
|
||||
import { MCPMetadataToolsService } from 'src/engine/api/mcp/services/tools/mcp-metadata-tools.service';
|
||||
import { validationSchemaManager } from 'src/engine/api/mcp/utils/get-json-schema';
|
||||
import { ObjectName } from 'src/engine/api/rest/metadata/types/metadata-entity.type';
|
||||
|
||||
@Injectable()
|
||||
export class CreateToolsService {
|
||||
constructor(
|
||||
private readonly metadataQueryBuilderFactory: MetadataQueryBuilderFactory,
|
||||
private readonly mCPMetadataToolsService: MCPMetadataToolsService,
|
||||
) {}
|
||||
|
||||
get tools() {
|
||||
return [
|
||||
{
|
||||
name: 'create-field-metadata',
|
||||
description: 'Create a new field metadata',
|
||||
inputSchema:
|
||||
this.mCPMetadataToolsService.mergeSchemaWithCommonProperties(
|
||||
validationSchemaManager.getSchemas().CreateFieldInput,
|
||||
),
|
||||
execute: (request: Request) => this.execute(request, 'fields'),
|
||||
},
|
||||
{
|
||||
name: 'create-object-metadata',
|
||||
description: 'Create a new object metadata',
|
||||
inputSchema:
|
||||
this.mCPMetadataToolsService.mergeSchemaWithCommonProperties(
|
||||
validationSchemaManager.getSchemas().CreateObjectInput,
|
||||
),
|
||||
execute: (request: Request) => this.execute(request, 'objects'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async execute(request: Request, objectName: ObjectName) {
|
||||
const requestContext = {
|
||||
body: request.body.params.arguments,
|
||||
baseUrl: this.mCPMetadataToolsService.generateBaseUrl(request),
|
||||
path: `/rest/metadata/${objectName}`,
|
||||
headers: request.headers,
|
||||
};
|
||||
const response = await this.mCPMetadataToolsService.send(
|
||||
requestContext,
|
||||
await this.metadataQueryBuilderFactory.create(
|
||||
requestContext,
|
||||
pick(request.body.params.arguments, ['fields', 'objects']),
|
||||
),
|
||||
);
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
|
||||
import { MetadataQueryBuilderFactory } from 'src/engine/api/rest/metadata/query-builder/metadata-query-builder.factory';
|
||||
import { MCPMetadataToolsService } from 'src/engine/api/mcp/services/tools/mcp-metadata-tools.service';
|
||||
import { validationSchemaManager } from 'src/engine/api/mcp/utils/get-json-schema';
|
||||
import { ObjectName } from 'src/engine/api/rest/metadata/types/metadata-entity.type';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteToolsService {
|
||||
constructor(
|
||||
private readonly metadataQueryBuilderFactory: MetadataQueryBuilderFactory,
|
||||
private readonly mCPMetadataToolsService: MCPMetadataToolsService,
|
||||
) {}
|
||||
|
||||
get tools() {
|
||||
return [
|
||||
{
|
||||
name: 'delete-field-metadata',
|
||||
description: 'Delete a field metadata',
|
||||
inputSchema:
|
||||
this.mCPMetadataToolsService.mergeSchemaWithCommonProperties(
|
||||
validationSchemaManager.getSchemas().DeleteOneFieldInput,
|
||||
),
|
||||
execute: (request: Request) => this.execute(request, 'fields'),
|
||||
},
|
||||
{
|
||||
name: 'delete-object-metadata',
|
||||
description: 'Delete an object metadata',
|
||||
inputSchema:
|
||||
this.mCPMetadataToolsService.mergeSchemaWithCommonProperties(
|
||||
validationSchemaManager.getSchemas().DeleteOneObjectInput,
|
||||
),
|
||||
execute: (request: Request) => this.execute(request, 'objects'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async execute(request: Request, objectName: ObjectName) {
|
||||
const requestContext = {
|
||||
body: request.body.params.arguments,
|
||||
baseUrl: this.mCPMetadataToolsService.generateBaseUrl(request),
|
||||
path: `/rest/metadata/${objectName}/${request.body.params.arguments.id}`,
|
||||
headers: request.headers,
|
||||
};
|
||||
|
||||
const response = await this.mCPMetadataToolsService.send(
|
||||
requestContext,
|
||||
await this.metadataQueryBuilderFactory.delete(requestContext),
|
||||
);
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,95 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
import pick from 'lodash.pick';
|
||||
|
||||
import { MetadataQueryBuilderFactory } from 'src/engine/api/rest/metadata/query-builder/metadata-query-builder.factory';
|
||||
import { MCPMetadataToolsService } from 'src/engine/api/mcp/services/tools/mcp-metadata-tools.service';
|
||||
import { ObjectName } from 'src/engine/api/rest/metadata/types/metadata-entity.type';
|
||||
|
||||
@Injectable()
|
||||
export class GetToolsService {
|
||||
constructor(
|
||||
private readonly metadataQueryBuilderFactory: MetadataQueryBuilderFactory,
|
||||
private readonly mCPMetadataToolsService: MCPMetadataToolsService,
|
||||
) {}
|
||||
|
||||
get tools() {
|
||||
const validationSchema =
|
||||
this.mCPMetadataToolsService.mergeSchemaWithCommonProperties({
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
description: 'Unique identifier for the resource, in UUID format.',
|
||||
},
|
||||
limit: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
default: 100,
|
||||
description:
|
||||
'The maximum number of items to return in the response',
|
||||
},
|
||||
starting_after: {
|
||||
type: 'string',
|
||||
description:
|
||||
'A cursor for paginating results. Provide the starting_after value returned by the previous request to fetch subsequent items.',
|
||||
},
|
||||
ending_before: {
|
||||
type: 'string',
|
||||
description:
|
||||
'A cursor for paginating results. Provide the ending_before value returned by the previous request to fetch subsequent items.',
|
||||
},
|
||||
},
|
||||
dependencies: {
|
||||
starting_after: {
|
||||
not: {
|
||||
required: ['ending_before'],
|
||||
},
|
||||
},
|
||||
ending_before: {
|
||||
not: {
|
||||
required: ['starting_after'],
|
||||
},
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'get-field-metadata',
|
||||
description: 'Find fields metadata',
|
||||
inputSchema: validationSchema,
|
||||
execute: (request: Request) => this.execute(request, 'fields'),
|
||||
},
|
||||
{
|
||||
name: 'get-object-metadata',
|
||||
description: 'Find objects metadata',
|
||||
inputSchema: validationSchema,
|
||||
execute: (request: Request) => this.execute(request, 'objects'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async execute(request: Request, objectName: ObjectName) {
|
||||
const requestContext = {
|
||||
body: request.body.params.arguments,
|
||||
baseUrl: this.mCPMetadataToolsService.generateBaseUrl(request),
|
||||
path: `/rest/metadata/${objectName}${request.body.params.arguments.id ? `/${request.body.params.arguments.id}` : ''}`,
|
||||
query: request.body.params.arguments,
|
||||
headers: request.headers,
|
||||
};
|
||||
|
||||
const response = await this.mCPMetadataToolsService.send(
|
||||
requestContext,
|
||||
await this.metadataQueryBuilderFactory.get(
|
||||
requestContext,
|
||||
pick(request.body.params.arguments, ['fields', 'objects']),
|
||||
),
|
||||
);
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
import { JSONSchema7 } from 'json-schema';
|
||||
|
||||
import { RequestContext } from 'src/engine/api/rest/types/RequestContext';
|
||||
import {
|
||||
RestApiService,
|
||||
GraphqlApiType,
|
||||
} from 'src/engine/api/rest/rest-api.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { getServerUrl } from 'src/utils/get-server-url';
|
||||
import { Query } from 'src/engine/api/rest/core/types/query.type';
|
||||
|
||||
@Injectable()
|
||||
export class MCPMetadataToolsService {
|
||||
constructor(
|
||||
protected readonly restApiService: RestApiService,
|
||||
protected readonly twentyConfigService: TwentyConfigService,
|
||||
) {}
|
||||
|
||||
mergeSchemaWithCommonProperties(schema: JSONSchema7) {
|
||||
return {
|
||||
...schema,
|
||||
properties: {
|
||||
...schema.properties,
|
||||
fields: {
|
||||
$ref: '#/result/commonProperties/fields',
|
||||
},
|
||||
objects: {
|
||||
$ref: '#/result/commonProperties/objects',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
generateBaseUrl(request: Request) {
|
||||
return getServerUrl(
|
||||
this.twentyConfigService.get('SERVER_URL'),
|
||||
`${request.protocol}://${request.get('host')}`,
|
||||
);
|
||||
}
|
||||
|
||||
async send(requestContext: RequestContext, data: Query) {
|
||||
return await this.restApiService.call(
|
||||
GraphqlApiType.METADATA,
|
||||
requestContext,
|
||||
data,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
import omit from 'lodash.omit';
|
||||
import pick from 'lodash.pick';
|
||||
|
||||
import { MetadataQueryBuilderFactory } from 'src/engine/api/rest/metadata/query-builder/metadata-query-builder.factory';
|
||||
import { MCPMetadataToolsService } from 'src/engine/api/mcp/services/tools/mcp-metadata-tools.service';
|
||||
import { validationSchemaManager } from 'src/engine/api/mcp/utils/get-json-schema';
|
||||
import { ObjectName } from 'src/engine/api/rest/metadata/types/metadata-entity.type';
|
||||
|
||||
@Injectable()
|
||||
export class UpdateToolsService {
|
||||
constructor(
|
||||
private readonly metadataQueryBuilderFactory: MetadataQueryBuilderFactory,
|
||||
private readonly mCPMetadataToolsService: MCPMetadataToolsService,
|
||||
) {}
|
||||
|
||||
get tools() {
|
||||
return [
|
||||
{
|
||||
name: 'update-field-metadata',
|
||||
description: 'Update a field metadata',
|
||||
inputSchema:
|
||||
this.mCPMetadataToolsService.mergeSchemaWithCommonProperties({
|
||||
...validationSchemaManager.getSchemas().UpdateOneFieldMetadataInput,
|
||||
properties: omit(
|
||||
validationSchemaManager.getSchemas().FieldMetadataDTO.properties,
|
||||
[
|
||||
'id',
|
||||
'type',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'isCustom',
|
||||
'standardOverrides',
|
||||
],
|
||||
),
|
||||
}),
|
||||
execute: (request: Request) => this.execute(request, 'fields'),
|
||||
},
|
||||
{
|
||||
name: 'update-object-metadata',
|
||||
description: 'Update an object metadata',
|
||||
inputSchema:
|
||||
this.mCPMetadataToolsService.mergeSchemaWithCommonProperties(
|
||||
validationSchemaManager.getSchemas().UpdateOneObjectInput,
|
||||
),
|
||||
execute: (request: Request) => this.execute(request, 'objects'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async execute(request: Request, objectName: ObjectName) {
|
||||
const { id, ...body } = request.body.params.arguments;
|
||||
const requestContext = {
|
||||
body,
|
||||
baseUrl: this.mCPMetadataToolsService.generateBaseUrl(request),
|
||||
path: `/rest/metadata/${objectName}/${id}`,
|
||||
headers: request.headers,
|
||||
};
|
||||
|
||||
const response = await this.mCPMetadataToolsService.send(
|
||||
requestContext,
|
||||
await this.metadataQueryBuilderFactory.update(
|
||||
requestContext,
|
||||
pick(request.body.params.arguments, ['fields', 'objects']),
|
||||
),
|
||||
);
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
import { validationMetadatasToSchemas } from 'class-validator-jsonschema';
|
||||
import { JSONSchema7 } from 'json-schema';
|
||||
|
||||
class ValidationSchemaManager {
|
||||
private static instance: ValidationSchemaManager;
|
||||
private schemas: Record<string, JSONSchema7> | null = null;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): ValidationSchemaManager {
|
||||
if (!ValidationSchemaManager.instance) {
|
||||
ValidationSchemaManager.instance = new ValidationSchemaManager();
|
||||
}
|
||||
|
||||
return ValidationSchemaManager.instance;
|
||||
}
|
||||
|
||||
public getSchemas(): Record<string, JSONSchema7> {
|
||||
if (!this.schemas) {
|
||||
this.schemas = validationMetadatasToSchemas() as Record<
|
||||
string,
|
||||
JSONSchema7
|
||||
>;
|
||||
}
|
||||
|
||||
return this.schemas;
|
||||
}
|
||||
}
|
||||
|
||||
export const validationSchemaManager = ValidationSchemaManager.getInstance();
|
||||
@ -1,11 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
import { RequestContext } from 'src/engine/api/rest/types/RequestContext';
|
||||
|
||||
@Injectable()
|
||||
export class EndingBeforeInputFactory {
|
||||
create(request: Request): string | undefined {
|
||||
const cursorQuery = request.query.ending_before;
|
||||
create(request: RequestContext): string | undefined {
|
||||
const cursorQuery = request.query?.ending_before;
|
||||
|
||||
if (typeof cursorQuery !== 'string') {
|
||||
return undefined;
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
import { RequestContext } from 'src/engine/api/rest/types/RequestContext';
|
||||
|
||||
@Injectable()
|
||||
export class LimitInputFactory {
|
||||
create(request: Request, defaultLimit = 60): number {
|
||||
if (!request.query.limit) {
|
||||
create(request: RequestContext, defaultLimit = 60): number {
|
||||
if (!request.query?.limit) {
|
||||
return defaultLimit;
|
||||
}
|
||||
const limit = +request.query.limit;
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
import { RequestContext } from 'src/engine/api/rest/types/RequestContext';
|
||||
|
||||
@Injectable()
|
||||
export class StartingAfterInputFactory {
|
||||
create(request: Request): string | undefined {
|
||||
const cursorQuery = request.query.starting_after;
|
||||
create(request: RequestContext): string | undefined {
|
||||
const cursorQuery = request.query?.starting_after;
|
||||
|
||||
if (typeof cursorQuery !== 'string') {
|
||||
return undefined;
|
||||
|
||||
@ -3,16 +3,25 @@ import { Injectable } from '@nestjs/common';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
import { fetchMetadataFields } from 'src/engine/api/rest/metadata/query-builder/utils/fetch-metadata-fields.utils';
|
||||
import {
|
||||
ObjectName,
|
||||
Singular,
|
||||
} from 'src/engine/api/rest/metadata/types/metadata-entity.type';
|
||||
import { Selectors } from 'src/engine/api/rest/metadata/types/metadata-query.type';
|
||||
|
||||
@Injectable()
|
||||
export class CreateMetadataQueryFactory {
|
||||
create(objectNameSingular: string, objectNamePlural: string): string {
|
||||
create(
|
||||
objectNameSingular: Singular<ObjectName>,
|
||||
objectNamePlural: ObjectName,
|
||||
selectors: Selectors,
|
||||
): string {
|
||||
const objectNameCapitalized = capitalize(objectNameSingular);
|
||||
|
||||
const fields = fetchMetadataFields(objectNamePlural);
|
||||
const fields = fetchMetadataFields(objectNamePlural, selectors);
|
||||
|
||||
return `
|
||||
mutation Create${objectNameCapitalized}($input: CreateOne${objectNameCapitalized}${
|
||||
mutation CreateOne${objectNameCapitalized}($input: CreateOne${objectNameCapitalized}${
|
||||
objectNameSingular === 'field' ? 'Metadata' : ''
|
||||
}Input!) {
|
||||
createOne${objectNameCapitalized}(input: $input) {
|
||||
|
||||
@ -2,9 +2,14 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
ObjectName,
|
||||
Singular,
|
||||
} from 'src/engine/api/rest/metadata/types/metadata-entity.type';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteMetadataQueryFactory {
|
||||
create(objectNameSingular: string): string {
|
||||
create(objectNameSingular: Singular<ObjectName>): string {
|
||||
const objectNameCapitalized = capitalize(objectNameSingular);
|
||||
const formattedObjectName =
|
||||
objectNameCapitalized === 'RelationMetadata'
|
||||
|
||||
@ -3,12 +3,13 @@ import { Injectable } from '@nestjs/common';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
import { fetchMetadataFields } from 'src/engine/api/rest/metadata/query-builder/utils/fetch-metadata-fields.utils';
|
||||
import { ObjectName } from 'src/engine/api/rest/metadata/types/metadata-entity.type';
|
||||
import { Selectors } from 'src/engine/api/rest/metadata/types/metadata-query.type';
|
||||
|
||||
@Injectable()
|
||||
export class FindManyMetadataQueryFactory {
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
create(objectNamePlural): string {
|
||||
const fields = fetchMetadataFields(objectNamePlural);
|
||||
create(objectNamePlural: ObjectName, selectors: Selectors): string {
|
||||
const fields = fetchMetadataFields(objectNamePlural, selectors);
|
||||
|
||||
return `
|
||||
query FindMany${capitalize(objectNamePlural)}(
|
||||
|
||||
@ -3,11 +3,20 @@ import { Injectable } from '@nestjs/common';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
import { fetchMetadataFields } from 'src/engine/api/rest/metadata/query-builder/utils/fetch-metadata-fields.utils';
|
||||
import {
|
||||
ObjectName,
|
||||
Singular,
|
||||
} from 'src/engine/api/rest/metadata/types/metadata-entity.type';
|
||||
import { Selectors } from 'src/engine/api/rest/metadata/types/metadata-query.type';
|
||||
|
||||
@Injectable()
|
||||
export class FindOneMetadataQueryFactory {
|
||||
create(objectNameSingular: string, objectNamePlural: string): string {
|
||||
const fields = fetchMetadataFields(objectNamePlural);
|
||||
create(
|
||||
objectNameSingular: Singular<ObjectName>,
|
||||
objectNamePlural: ObjectName,
|
||||
selectors: Selectors,
|
||||
): string {
|
||||
const fields = fetchMetadataFields(objectNamePlural, selectors);
|
||||
|
||||
return `
|
||||
query FindOne${capitalize(objectNameSingular)}(
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
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';
|
||||
import { RequestContext } from 'src/engine/api/rest/types/RequestContext';
|
||||
|
||||
@Injectable()
|
||||
export class GetMetadataVariablesFactory {
|
||||
@ -15,14 +14,17 @@ export class GetMetadataVariablesFactory {
|
||||
private readonly limitInputFactory: LimitInputFactory,
|
||||
) {}
|
||||
|
||||
create(id: string | undefined, request: Request): MetadataQueryVariables {
|
||||
create(
|
||||
id: string | undefined,
|
||||
requestContext: RequestContext,
|
||||
): MetadataQueryVariables {
|
||||
if (id) {
|
||||
return { id };
|
||||
}
|
||||
|
||||
const limit = this.limitInputFactory.create(request, 1000);
|
||||
const before = this.endingBeforeInputFactory.create(request);
|
||||
const after = this.startingAfterInputFactory.create(request);
|
||||
const limit = this.limitInputFactory.create(requestContext, 1000);
|
||||
const before = this.endingBeforeInputFactory.create(requestContext);
|
||||
const after = this.startingAfterInputFactory.create(requestContext);
|
||||
|
||||
if (before && after) {
|
||||
throw new BadRequestException(
|
||||
|
||||
@ -3,13 +3,22 @@ import { Injectable } from '@nestjs/common';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
import { fetchMetadataFields } from 'src/engine/api/rest/metadata/query-builder/utils/fetch-metadata-fields.utils';
|
||||
import {
|
||||
ObjectName,
|
||||
Singular,
|
||||
} from 'src/engine/api/rest/metadata/types/metadata-entity.type';
|
||||
import { Selectors } from 'src/engine/api/rest/metadata/types/metadata-query.type';
|
||||
|
||||
@Injectable()
|
||||
export class UpdateMetadataQueryFactory {
|
||||
create(objectNameSingular: string, objectNamePlural: string): string {
|
||||
create(
|
||||
objectNameSingular: Singular<ObjectName>,
|
||||
objectNamePlural: ObjectName,
|
||||
selectors: Selectors,
|
||||
): string {
|
||||
const objectNameCapitalized = capitalize(objectNameSingular);
|
||||
|
||||
const fields = fetchMetadataFields(objectNamePlural);
|
||||
const fields = fetchMetadataFields(objectNamePlural, selectors);
|
||||
|
||||
return `
|
||||
mutation Update${objectNameCapitalized}($input: UpdateOne${objectNameCapitalized}${
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
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';
|
||||
@ -9,7 +7,11 @@ import { parseMetadataPath } from 'src/engine/api/rest/metadata/query-builder/ut
|
||||
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';
|
||||
import {
|
||||
MetadataQuery,
|
||||
Selectors,
|
||||
} from 'src/engine/api/rest/metadata/types/metadata-query.type';
|
||||
import { RequestContext } from 'src/engine/api/rest/types/RequestContext';
|
||||
|
||||
@Injectable()
|
||||
export class MetadataQueryBuilderFactory {
|
||||
@ -22,37 +24,53 @@ export class MetadataQueryBuilderFactory {
|
||||
private readonly getMetadataVariablesFactory: GetMetadataVariablesFactory,
|
||||
) {}
|
||||
|
||||
async get(request: Request): Promise<MetadataQuery> {
|
||||
const { id, objectNameSingular, objectNamePlural } =
|
||||
parseMetadataPath(request);
|
||||
async get(
|
||||
request: RequestContext,
|
||||
selectors?: Selectors,
|
||||
): Promise<MetadataQuery> {
|
||||
const { id, objectNameSingular, objectNamePlural } = parseMetadataPath(
|
||||
request.path,
|
||||
);
|
||||
|
||||
return {
|
||||
query: id
|
||||
? this.findOneQueryFactory.create(objectNameSingular, objectNamePlural)
|
||||
: this.findManyQueryFactory.create(objectNamePlural),
|
||||
? this.findOneQueryFactory.create(
|
||||
objectNameSingular,
|
||||
objectNamePlural,
|
||||
selectors,
|
||||
)
|
||||
: this.findManyQueryFactory.create(objectNamePlural, selectors),
|
||||
variables: this.getMetadataVariablesFactory.create(id, request),
|
||||
};
|
||||
}
|
||||
|
||||
async create(request: Request): Promise<MetadataQuery> {
|
||||
const { objectNameSingular, objectNamePlural } = parseMetadataPath(request);
|
||||
async create(
|
||||
{ path, body }: Pick<RequestContext, 'path' | 'body'>,
|
||||
selectors?: Selectors,
|
||||
): Promise<MetadataQuery> {
|
||||
const { objectNameSingular, objectNamePlural } = parseMetadataPath(path);
|
||||
|
||||
return {
|
||||
query: this.createQueryFactory.create(
|
||||
objectNameSingular,
|
||||
objectNamePlural,
|
||||
selectors,
|
||||
),
|
||||
variables: {
|
||||
input: {
|
||||
[objectNameSingular]: request.body,
|
||||
[objectNameSingular]: body,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async update(request: Request): Promise<MetadataQuery> {
|
||||
const { objectNameSingular, objectNamePlural, id } =
|
||||
parseMetadataPath(request);
|
||||
async update(
|
||||
request: Pick<RequestContext, 'path' | 'body'>,
|
||||
selectors?: Selectors,
|
||||
): Promise<MetadataQuery> {
|
||||
const { objectNameSingular, objectNamePlural, id } = parseMetadataPath(
|
||||
request.path,
|
||||
);
|
||||
|
||||
if (!id) {
|
||||
throw new BadRequestException(
|
||||
@ -64,6 +82,7 @@ export class MetadataQueryBuilderFactory {
|
||||
query: this.updateQueryFactory.create(
|
||||
objectNameSingular,
|
||||
objectNamePlural,
|
||||
selectors,
|
||||
),
|
||||
variables: {
|
||||
input: {
|
||||
@ -74,8 +93,10 @@ export class MetadataQueryBuilderFactory {
|
||||
};
|
||||
}
|
||||
|
||||
async delete(request: Request): Promise<MetadataQuery> {
|
||||
const { objectNameSingular, id } = parseMetadataPath(request);
|
||||
async delete(
|
||||
request: Pick<RequestContext, 'path' | 'body'>,
|
||||
): Promise<MetadataQuery> {
|
||||
const { objectNameSingular, id } = parseMetadataPath(request.path);
|
||||
|
||||
if (!id) {
|
||||
throw new BadRequestException(
|
||||
|
||||
@ -4,7 +4,7 @@ describe('parseMetadataPath', () => {
|
||||
it('should parse object from request path with uuid', () => {
|
||||
const request: any = { path: '/rest/metadata/fields/uuid' };
|
||||
|
||||
expect(parseMetadataPath(request)).toEqual({
|
||||
expect(parseMetadataPath(request.path)).toEqual({
|
||||
objectNameSingular: 'field',
|
||||
objectNamePlural: 'fields',
|
||||
id: 'uuid',
|
||||
@ -14,7 +14,7 @@ describe('parseMetadataPath', () => {
|
||||
it('should parse object from request path', () => {
|
||||
const request: any = { path: '/rest/metadata/fields' };
|
||||
|
||||
expect(parseMetadataPath(request)).toEqual({
|
||||
expect(parseMetadataPath(request.path)).toEqual({
|
||||
objectNameSingular: 'field',
|
||||
objectNamePlural: 'fields',
|
||||
id: undefined,
|
||||
@ -24,7 +24,7 @@ describe('parseMetadataPath', () => {
|
||||
it('should throw for wrong request path', () => {
|
||||
const request: any = { path: '/rest/metadata/INVALID' };
|
||||
|
||||
expect(() => parseMetadataPath(request)).toThrow(
|
||||
expect(() => parseMetadataPath(request.path)).toThrow(
|
||||
'Query path \'/rest/metadata/INVALID\' invalid. Metadata path "INVALID" does not exist. Valid examples: /rest/metadata/fields or /rest/metadata/objects',
|
||||
);
|
||||
});
|
||||
@ -32,7 +32,7 @@ describe('parseMetadataPath', () => {
|
||||
it('should throw for wrong request path', () => {
|
||||
const request: any = { path: '/rest/metadata/fields/uuid/toto' };
|
||||
|
||||
expect(() => parseMetadataPath(request)).toThrow(
|
||||
expect(() => parseMetadataPath(request.path)).toThrow(
|
||||
"Query path '/rest/metadata/fields/uuid/toto' invalid. Valid examples: /rest/metadata/fields or /rest/metadata/objects/id",
|
||||
);
|
||||
});
|
||||
|
||||
@ -1,68 +1,87 @@
|
||||
export const fetchMetadataFields = (objectNamePlural: string) => {
|
||||
const fields = `
|
||||
type
|
||||
name
|
||||
label
|
||||
description
|
||||
icon
|
||||
isCustom
|
||||
isActive
|
||||
isSystem
|
||||
isNullable
|
||||
createdAt
|
||||
updatedAt
|
||||
defaultValue
|
||||
options
|
||||
relation {
|
||||
type
|
||||
targetObjectMetadata {
|
||||
id
|
||||
nameSingular
|
||||
namePlural
|
||||
}
|
||||
targetFieldMetadata {
|
||||
id
|
||||
name
|
||||
}
|
||||
sourceObjectMetadata {
|
||||
id
|
||||
nameSingular
|
||||
namePlural
|
||||
}
|
||||
sourceFieldMetadata {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`;
|
||||
import { Selectors } from 'src/engine/api/rest/metadata/types/metadata-query.type';
|
||||
|
||||
export const fetchMetadataFields = (
|
||||
objectNamePlural: string,
|
||||
selector: Selectors,
|
||||
) => {
|
||||
const defaultFields = `
|
||||
type
|
||||
name
|
||||
label
|
||||
description
|
||||
icon
|
||||
isCustom
|
||||
isActive
|
||||
isSystem
|
||||
isNullable
|
||||
createdAt
|
||||
updatedAt
|
||||
defaultValue
|
||||
options
|
||||
relation {
|
||||
type
|
||||
targetObjectMetadata {
|
||||
id
|
||||
nameSingular
|
||||
namePlural
|
||||
}
|
||||
targetFieldMetadata {
|
||||
id
|
||||
name
|
||||
}
|
||||
sourceObjectMetadata {
|
||||
id
|
||||
nameSingular
|
||||
namePlural
|
||||
}
|
||||
sourceFieldMetadata {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const fieldsSelection = selector?.fields?.join('\n') ?? defaultFields;
|
||||
|
||||
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 'objects': {
|
||||
const objectsSelection =
|
||||
selector?.objects?.join('\n') ??
|
||||
`
|
||||
dataSourceId
|
||||
nameSingular
|
||||
namePlural
|
||||
labelSingular
|
||||
labelPlural
|
||||
description
|
||||
icon
|
||||
isCustom
|
||||
isActive
|
||||
isSystem
|
||||
createdAt
|
||||
updatedAt
|
||||
labelIdentifierFieldMetadataId
|
||||
imageIdentifierFieldMetadataId
|
||||
`;
|
||||
|
||||
const fieldsPart = selector?.fields
|
||||
? `
|
||||
fields(paging: { first: 1000 }) {
|
||||
edges {
|
||||
node {
|
||||
${fieldsSelection}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
`
|
||||
: '';
|
||||
|
||||
return `
|
||||
${objectsSelection}
|
||||
${fieldsPart}
|
||||
`;
|
||||
}
|
||||
case 'fields':
|
||||
return fields;
|
||||
return fieldsSelection;
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,51 +1,48 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
import {
|
||||
ObjectName,
|
||||
ObjectNameSingularAndPlural,
|
||||
} from 'src/engine/api/rest/metadata/types/metadata-entity.type';
|
||||
|
||||
const getObjectNames = (
|
||||
objectName: ObjectName,
|
||||
): ObjectNameSingularAndPlural => {
|
||||
return {
|
||||
objectNameSingular: objectName.substring(
|
||||
0,
|
||||
objectName.length - 1,
|
||||
) as ObjectNameSingularAndPlural['objectNameSingular'],
|
||||
objectNamePlural: objectName,
|
||||
};
|
||||
};
|
||||
|
||||
export const parseMetadataPath = (
|
||||
request: Request,
|
||||
): { objectNameSingular: string; objectNamePlural: string; id?: string } => {
|
||||
const queryAction = request.path.replace('/rest/metadata/', '').split('/');
|
||||
path: string,
|
||||
): ObjectNameSingularAndPlural => {
|
||||
const queryAction = 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`,
|
||||
`Query path '${path}' invalid. Valid examples: /rest/metadata/fields or /rest/metadata/objects/id`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!['fields', 'objects'].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`,
|
||||
`Query path '${path}' invalid. Metadata path "${queryAction[0]}" does not exist. Valid examples: /rest/metadata/fields or /rest/metadata/objects`,
|
||||
);
|
||||
}
|
||||
|
||||
const objectName = queryAction[0];
|
||||
const hasId = queryAction.length === 2;
|
||||
|
||||
if (queryAction.length === 2) {
|
||||
switch (objectName) {
|
||||
case 'fields':
|
||||
return {
|
||||
objectNameSingular: 'field',
|
||||
objectNamePlural: 'fields',
|
||||
id: queryAction[1],
|
||||
};
|
||||
case 'objects':
|
||||
return {
|
||||
objectNameSingular: 'object',
|
||||
objectNamePlural: 'objects',
|
||||
id: queryAction[1],
|
||||
};
|
||||
default:
|
||||
return { objectNameSingular: '', objectNamePlural: '', id: '' };
|
||||
}
|
||||
} else {
|
||||
switch (objectName) {
|
||||
case 'fields':
|
||||
return { objectNameSingular: 'field', objectNamePlural: 'fields' };
|
||||
case 'objects':
|
||||
return { objectNameSingular: 'object', objectNamePlural: 'objects' };
|
||||
default:
|
||||
return { objectNameSingular: '', objectNamePlural: '' };
|
||||
}
|
||||
}
|
||||
const { objectNameSingular, objectNamePlural } = getObjectNames(
|
||||
queryAction[0] as ObjectName,
|
||||
);
|
||||
|
||||
return {
|
||||
objectNameSingular,
|
||||
objectNamePlural,
|
||||
...(hasId ? { id: queryAction[1] } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
export type Singular<S extends string> = S extends `${infer Stem}s` ? Stem : S;
|
||||
|
||||
export type ObjectName = 'fields' | 'objects';
|
||||
|
||||
export type ObjectNameSingularAndPlural<
|
||||
ObjectNameGeneric extends ObjectName = ObjectName,
|
||||
> = {
|
||||
objectNameSingular: Singular<ObjectNameGeneric>;
|
||||
objectNamePlural: ObjectNameGeneric;
|
||||
id?: string;
|
||||
};
|
||||
@ -4,3 +4,7 @@ export type MetadataQuery = {
|
||||
query: string;
|
||||
variables: MetadataQueryVariables;
|
||||
};
|
||||
|
||||
export type Selectors =
|
||||
| { fields?: Array<string>; objects?: Array<string> }
|
||||
| undefined;
|
||||
|
||||
@ -19,5 +19,6 @@ import { RestApiMetadataController } from 'src/engine/api/rest/metadata/rest-api
|
||||
],
|
||||
controllers: [RestApiMetadataController],
|
||||
providers: [RestApiService, RestApiMetadataService],
|
||||
exports: [RestApiMetadataService, RestApiService],
|
||||
})
|
||||
export class RestApiModule {}
|
||||
|
||||
@ -2,12 +2,10 @@ import { HttpService } from '@nestjs/axios';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { Request } from 'express';
|
||||
|
||||
import { Query } from 'src/engine/api/rest/core/types/query.type';
|
||||
import { RestApiException } from 'src/engine/api/rest/errors/RestApiException';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { getServerUrl } from 'src/utils/get-server-url';
|
||||
import { RequestContext } from 'src/engine/api/rest/types/RequestContext';
|
||||
|
||||
export enum GraphqlApiType {
|
||||
CORE = 'core',
|
||||
@ -16,18 +14,15 @@ export enum GraphqlApiType {
|
||||
|
||||
@Injectable()
|
||||
export class RestApiService {
|
||||
constructor(
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
private readonly httpService: HttpService,
|
||||
) {}
|
||||
constructor(private readonly httpService: HttpService) {}
|
||||
|
||||
async call(graphqlApiType: GraphqlApiType, request: Request, data: Query) {
|
||||
const baseUrl = getServerUrl(
|
||||
request,
|
||||
this.twentyConfigService.get('SERVER_URL'),
|
||||
);
|
||||
async call(
|
||||
graphqlApiType: GraphqlApiType,
|
||||
requestContext: RequestContext,
|
||||
data: Query,
|
||||
) {
|
||||
let response: AxiosResponse;
|
||||
const url = `${baseUrl}/${
|
||||
const url = `${requestContext.baseUrl}/${
|
||||
graphqlApiType === GraphqlApiType.CORE
|
||||
? 'graphql'
|
||||
: GraphqlApiType.METADATA
|
||||
@ -37,7 +32,7 @@ export class RestApiService {
|
||||
response = await this.httpService.axiosRef.post(url, data, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: request.headers.authorization,
|
||||
Authorization: requestContext.headers.authorization,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import { Request } from 'express';
|
||||
|
||||
export type RequestContext = {
|
||||
headers: Request['headers'];
|
||||
baseUrl: string;
|
||||
path: Request['path'];
|
||||
body?: Request['body'];
|
||||
query?: Request['query'];
|
||||
};
|
||||
Reference in New Issue
Block a user