fix(ai): expose MORPH_RELATION join columns in AI/MCP tool schemas (#21012)
## Summary
- Fixes a bug where `noteTarget` (and any other morph-relation join
object) created via AI/MCP would land with `targetCompanyId` /
`targetPersonId` / `targetOpportunityId` left null, even though the tool
reported success.
- Root cause: the Zod schema generators for the AI tools only branched
on `FieldMetadataType.RELATION`. MORPH_RELATION fields fell through to
the default case — for `create_*` they were exposed as `targetCompany:
string` instead of `targetCompanyId: uuid`, and for `group_by_*` they
were silently skipped entirely. Downstream
(`data-arg-processor.service.ts` and the group-by arg processor) already
accept the join-column form for both kinds of relations via
`computeMorphOrRelationFieldJoinColumnName` and
`isMorphOrRelationFlatFieldMetadata`, so the fix is purely in the schema
generators.
## Changes
- `record-properties.zod-schema.ts` — extend the existing RELATION
MANY_TO_ONE / ONE_TO_MANY branches to also match MORPH_RELATION.
- `group-by-tool.zod-schema.ts` — replace the silent MORPH_RELATION skip
with the same treatment as RELATION MANY_TO_ONE (exposes `${name}Id` as
a groupBy option).
- `test/integration/ai/suites/mcp-tool-execution.integration-spec.ts` —
new file. First integration test for tool execution end-to-end. Drives
the real MCP JSON-RPC endpoint with the seeded API key (`learn_tools`
for schema introspection, `execute_tool` for invocation):
- asserts `create_note_target`'s schema exposes `targetCompanyId` /
`targetPersonId` / `targetOpportunityId` as UUIDs and does **not**
expose `targetCompany` / `targetPerson` / `targetOpportunity`.
- creates a company + note + noteTarget via MCP, then queries the
workspace schema to confirm `targetCompanyId` is actually persisted in
the FK column.
- asserts `group_by_note_targets` schema accepts `targetCompanyId` as a
groupBy key.
- sets up 3 noteTargets (2 → company A, 1 → company B), calls
`group_by_note_targets` by `targetCompanyId`, and asserts the counts.
Out of scope: `record-filter.zod-schema.ts` has the same pattern (only
RELATION) — left for a follow-up so this PR stays focused on what was
reported.
## Test plan
- [x] `npx nx typecheck twenty-server`
- [x] `npx oxlint --type-aware` on changed files — clean
- [x] `npx oxfmt --check` on changed files — clean
- [x] Integration tests pass (4/4) after `database:reset`:
- `should expose the morph-relation join columns as \`${name}Id\` UUID
parameters`
- `should persist targetCompanyId when create_note_target is invoked via
MCP`
- `should expose targetCompanyId as a valid groupBy option`
- `should group noteTargets by targetCompanyId via MCP`
This commit is contained in:
+5
-5
@@ -66,7 +66,11 @@ const buildGroupByEntriesAndDescriptions = (
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION)) {
|
||||
const isRelationOrMorphRelation =
|
||||
isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION) ||
|
||||
isFieldMetadataEntityOfType(field, FieldMetadataType.MORPH_RELATION);
|
||||
|
||||
if (isRelationOrMorphRelation) {
|
||||
if (field.settings?.relationType === RelationType.MANY_TO_ONE) {
|
||||
const relationFieldName = `${field.name}Id`;
|
||||
|
||||
@@ -79,10 +83,6 @@ const buildGroupByEntriesAndDescriptions = (
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isFieldMetadataEntityOfType(field, FieldMetadataType.MORPH_RELATION)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isFieldMetadataDateKind(field.type)) {
|
||||
groupByEntries.push(
|
||||
z.object({ [field.name]: dateGroupBySchema }).strict(),
|
||||
|
||||
+6
-2
@@ -93,8 +93,12 @@ export const generateRecordPropertiesZodSchema = (
|
||||
return;
|
||||
}
|
||||
|
||||
const isRelationOrMorphRelation =
|
||||
isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION) ||
|
||||
isFieldMetadataEntityOfType(field, FieldMetadataType.MORPH_RELATION);
|
||||
|
||||
if (
|
||||
isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION) &&
|
||||
isRelationOrMorphRelation &&
|
||||
field.settings?.relationType === RelationType.MANY_TO_ONE
|
||||
) {
|
||||
const uuidSchema = z.uuidv4();
|
||||
@@ -107,7 +111,7 @@ export const generateRecordPropertiesZodSchema = (
|
||||
}
|
||||
|
||||
if (
|
||||
isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION) &&
|
||||
isRelationOrMorphRelation &&
|
||||
field.settings?.relationType === RelationType.ONE_TO_MANY
|
||||
) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user