diff --git a/CHANGELOG.md b/CHANGELOG.md index f66467f..e37507d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file. The format --- +## [Unreleased] + +[Unreleased]: https://github.com/czottmann/linearis/compare/v2025.12.3...HEAD + +### Added + +- New `projects create` command to create Linear projects + - Required `--team` flag (accepts team key, name, or UUID; comma-separated for multiple teams) + - Optional flags: `--description`, `--color`, `--icon`, `--lead`, `--target-date`, `--start-date`, `--state` + - Validates date format (YYYY-MM-DD) and state values before sending to API +- New `--sort-order` flag for `issues create` and `issues update` commands + - Allows controlling issue position relative to other issues (lower values = higher in list) + - Validates numeric input before sending to API + +### Fixed + +- `LinearProject` type now includes `startDate` field (was previously fetched but not exposed) + +--- + ## [2025.12.3] - 2025-12-11 [2025.12.3]: https://github.com/czottmann/linearis/compare/v2025.12.2...v2025.12.3 diff --git a/src/commands/issues.ts b/src/commands/issues.ts index e571b21..5e30409 100644 --- a/src/commands/issues.ts +++ b/src/commands/issues.ts @@ -131,6 +131,10 @@ export function setupIssuesCommands(program: Command): void { ) .option("--status ", "status name or ID") .option("--parent-ticket ", "parent issue ID or identifier") + .option( + "--sort-order ", + "position relative to other issues (lower = higher in list)", + ) .action( handleAsyncCommand( async (title: string, options: any, command: Command) => { @@ -143,6 +147,17 @@ export function setupIssuesCommands(program: Command): void { linearService, ); + // Validate sortOrder if provided + let sortOrder: number | undefined; + if (options.sortOrder !== undefined) { + sortOrder = parseFloat(options.sortOrder); + if (!Number.isFinite(sortOrder)) { + throw new Error( + `Invalid --sort-order value "${options.sortOrder}": must be a number`, + ); + } + } + // Prepare labels array if provided let labelIds: string[] | undefined; if (options.labels) { @@ -161,6 +176,7 @@ export function setupIssuesCommands(program: Command): void { parentId: options.parentTicket, // GraphQL service handles parent resolution milestoneId: options.projectMilestone, cycleId: options.cycle, + sortOrder, }; const result = await issuesService.createIssue(createArgs); @@ -252,6 +268,10 @@ export function setupIssuesCommands(program: Command): void { "set cycle (can use name or ID, will try to resolve within team context first)", ) .option("--clear-cycle", "clear existing cycle assignment") + .option( + "--sort-order ", + "position relative to other issues (lower = higher in list)", + ) .action( handleAsyncCommand( async (issueId: string, options: any, command: Command) => { @@ -314,6 +334,17 @@ export function setupIssuesCommands(program: Command): void { linearService, ); + // Validate sortOrder if provided + let sortOrder: number | undefined; + if (options.sortOrder !== undefined) { + sortOrder = parseFloat(options.sortOrder); + if (!Number.isFinite(sortOrder)) { + throw new Error( + `Invalid --sort-order value "${options.sortOrder}": must be a number`, + ); + } + } + // Prepare update arguments for GraphQL service let labelIds: string[] | undefined; if (options.clearLabels) { @@ -339,6 +370,7 @@ export function setupIssuesCommands(program: Command): void { milestoneId: options.projectMilestone || (options.clearProjectMilestone ? null : undefined), cycleId: options.cycle || (options.clearCycle ? null : undefined), + sortOrder, }; const labelMode = options.labelBy || "adding"; diff --git a/src/commands/projects.ts b/src/commands/projects.ts index c82b0f3..a78514b 100644 --- a/src/commands/projects.ts +++ b/src/commands/projects.ts @@ -1,4 +1,6 @@ import { Command } from "commander"; +import { createGraphQLService } from "../utils/graphql-service.js"; +import { GraphQLProjectsService } from "../utils/graphql-projects-service.js"; import { createLinearService } from "../utils/linear-service.js"; import { handleAsyncCommand, outputSuccess } from "../utils/output.js"; @@ -45,9 +47,98 @@ export function setupProjectsCommands(program: Command): void { .action(handleAsyncCommand(async (_options: any, command: Command) => { // Initialize Linear service for project operations const service = await createLinearService(command.parent!.parent!.opts()); - + // Fetch all projects with their relationships const result = await service.getProjects(); outputSuccess(result); })); + + /** + * Create a new project + * + * Command: `linearis projects create --team [options]` + * + * Creates a new project with the specified name and team. + * Team is required by the Linear API. + */ + projects + .command("create ") + .description("Create a new project") + .requiredOption( + "--team ", + "team key, name, or ID (required, can specify multiple with commas)", + ) + .option("-d, --description ", "project description") + .option("--color ", "project color (hex code)") + .option("--icon ", "project icon") + .option("--lead ", "project lead user ID (UUID)") + .option("--target-date ", "target completion date (YYYY-MM-DD)") + .option("--start-date ", "project start date (YYYY-MM-DD)") + .option( + "--state ", + "project state: planned, started, paused, completed, canceled", + ) + .action( + handleAsyncCommand(async (name: string, options: any, command: Command) => { + const [graphQLService, linearService] = await Promise.all([ + createGraphQLService(command.parent!.parent!.opts()), + createLinearService(command.parent!.parent!.opts()), + ]); + const projectsService = new GraphQLProjectsService( + graphQLService, + linearService, + ); + + // Parse team IDs (comma-separated) + const teamIds = options.team.split(",").map((t: string) => t.trim()); + + // Validate state if provided + const validStates = [ + "planned", + "started", + "paused", + "completed", + "canceled", + ]; + if (options.state && !validStates.includes(options.state)) { + throw new Error( + `Invalid --state "${options.state}". Must be one of: ${validStates.join(", ")}`, + ); + } + + // Validate date format (YYYY-MM-DD) + const isValidDate = (dateStr: string): boolean => { + if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) return false; + const date = new Date(dateStr); + return !isNaN(date.getTime()); + }; + + if (options.targetDate && !isValidDate(options.targetDate)) { + throw new Error( + `Invalid --target-date "${options.targetDate}". Must be YYYY-MM-DD format.`, + ); + } + + if (options.startDate && !isValidDate(options.startDate)) { + throw new Error( + `Invalid --start-date "${options.startDate}". Must be YYYY-MM-DD format.`, + ); + } + + const createArgs = { + name, + teamIds, + description: options.description, + color: options.color, + icon: options.icon, + leadId: options.lead, + targetDate: options.targetDate, + startDate: options.startDate, + state: options.state, + }; + + const result = await projectsService.createProject(createArgs); + outputSuccess(result); + }), + ); } diff --git a/src/queries/index.ts b/src/queries/index.ts index 8947595..1a27e4b 100644 --- a/src/queries/index.ts +++ b/src/queries/index.ts @@ -21,3 +21,6 @@ export * from "./documents.js"; // Attachment queries and mutations export * from "./attachments.js"; + +// Project queries and mutations +export * from "./projects.js"; diff --git a/src/queries/projects.ts b/src/queries/projects.ts new file mode 100644 index 0000000..46f00d5 --- /dev/null +++ b/src/queries/projects.ts @@ -0,0 +1,68 @@ +/** + * GraphQL queries and mutations for project operations + * + * Contains project-specific queries including the create mutation. + * Uses consistent response structure matching LinearProject type. + */ + +/** + * Project response fragment with all relationships + * + * Includes teams, lead, and all standard project fields. + * Used for consistent responses across project operations. + */ +export const PROJECT_FRAGMENT = ` + id + name + description + state + progress + teams { + nodes { + id + key + name + } + } + lead { + id + name + } + targetDate + startDate + createdAt + updatedAt +`; + +/** + * Create project mutation with complete response + * + * Creates a new project and returns complete project data including + * all relationships. TeamIds is required by the GraphQL API. + */ +export const CREATE_PROJECT_MUTATION = ` + mutation CreateProject($input: ProjectCreateInput!) { + projectCreate(input: $input) { + success + project { + ${PROJECT_FRAGMENT} + } + } + } +`; + +/** + * List projects query with filtering + * + * Fetches projects with optional team filtering. + * Returns complete project data for each result. + */ +export const LIST_PROJECTS_QUERY = ` + query ListProjects($first: Int!, $filter: ProjectFilter) { + projects(first: $first, filter: $filter) { + nodes { + ${PROJECT_FRAGMENT} + } + } + } +`; diff --git a/src/utils/graphql-issues-service.ts b/src/utils/graphql-issues-service.ts index 6a5e1fe..7ab9919 100644 --- a/src/utils/graphql-issues-service.ts +++ b/src/utils/graphql-issues-service.ts @@ -385,6 +385,7 @@ export class GraphQLIssuesService { if (finalLabelIds !== undefined) { updateInput.labelIds = finalLabelIds; } + if (args.sortOrder !== undefined) updateInput.sortOrder = args.sortOrder; const updateResult = await this.graphQLService.rawRequest( UPDATE_ISSUE_MUTATION, @@ -614,6 +615,7 @@ export class GraphQLIssuesService { if (finalParentId) createInput.parentId = finalParentId; if (finalMilestoneId) createInput.projectMilestoneId = finalMilestoneId; if (finalCycleId) createInput.cycleId = finalCycleId; + if (args.sortOrder !== undefined) createInput.sortOrder = args.sortOrder; const createResult = await this.graphQLService.rawRequest( CREATE_ISSUE_MUTATION, diff --git a/src/utils/graphql-projects-service.ts b/src/utils/graphql-projects-service.ts new file mode 100644 index 0000000..9ea7230 --- /dev/null +++ b/src/utils/graphql-projects-service.ts @@ -0,0 +1,189 @@ +import { GraphQLService } from "./graphql-service.js"; +import { LinearService } from "./linear-service.js"; +import { + CREATE_PROJECT_MUTATION, + LIST_PROJECTS_QUERY, +} from "../queries/projects.js"; +import { LinearProject, ProjectCreateArgs } from "./linear-types.js"; + +/** + * Raw project response from GraphQL API + * Transforms to LinearProject format + */ +interface GraphQLProjectResponse { + id: string; + name: string; + description?: string; + state: string; + progress: number; + teams: { + nodes: Array<{ + id: string; + key: string; + name: string; + }>; + }; + lead?: { + id: string; + name: string; + }; + targetDate?: string; + startDate?: string; + createdAt: string; + updatedAt: string; +} + +/** + * GraphQL-optimized projects service + * + * Provides create and list operations using optimized GraphQL queries. + * Uses smart ID resolution for teams and leads. + */ +export class GraphQLProjectsService { + constructor( + private graphqlService: GraphQLService, + private linearService: LinearService, + ) {} + + /** + * Transform GraphQL response to LinearProject format + */ + private transformProject(project: GraphQLProjectResponse): LinearProject { + return { + id: project.id, + name: project.name, + description: project.description || undefined, + state: project.state, + progress: project.progress, + teams: project.teams.nodes, + lead: project.lead || undefined, + targetDate: project.targetDate || undefined, + startDate: project.startDate || undefined, + createdAt: project.createdAt, + updatedAt: project.updatedAt, + }; + } + + /** + * Resolve team identifiers to UUIDs + * + * Accepts team keys (e.g., "ENG"), names (e.g., "Engineering"), or UUIDs. + * Returns array of resolved team UUIDs. + */ + private async resolveTeamIds(teamIds: string[]): Promise { + const resolvedIds: string[] = []; + + for (const teamId of teamIds) { + // If it looks like a UUID, use it directly + if (this.isUUID(teamId)) { + resolvedIds.push(teamId); + } else { + // Resolve team by key or name + const resolved = await this.linearService.resolveTeamId(teamId); + if (!resolved) { + throw new Error(`Team not found: "${teamId}"`); + } + resolvedIds.push(resolved); + } + } + + return resolvedIds; + } + + /** + * Validate user ID is a UUID + * + * Linear API requires UUID for leadId - no name resolution available. + */ + private validateUserId(userId: string): string { + if (!this.isUUID(userId)) { + throw new Error( + `Lead ID must be a UUID (got "${userId}"). Use 'linearis users list' to find user UUIDs.`, + ); + } + return userId; + } + + /** + * Check if string is a UUID + */ + private isUUID(str: string): boolean { + return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( + str, + ); + } + + /** + * Create a new project + * + * @param args Project creation arguments with human-friendly identifiers + * @returns Created project with all fields + */ + async createProject(args: ProjectCreateArgs): Promise { + // Resolve team IDs + const resolvedTeamIds = await this.resolveTeamIds(args.teamIds); + + // Validate lead ID if provided (must be UUID) + const validatedLeadId = args.leadId + ? this.validateUserId(args.leadId) + : undefined; + + // Build the GraphQL input + const input: Record = { + name: args.name, + teamIds: resolvedTeamIds, + }; + + if (args.description !== undefined) input.description = args.description; + if (args.color !== undefined) input.color = args.color; + if (args.icon !== undefined) input.icon = args.icon; + if (validatedLeadId !== undefined) input.leadId = validatedLeadId; + if (args.targetDate !== undefined) input.targetDate = args.targetDate; + if (args.startDate !== undefined) input.startDate = args.startDate; + if (args.state !== undefined) input.state = args.state; + + const result = await this.graphqlService.rawRequest<{ + projectCreate: { success: boolean; project: GraphQLProjectResponse }; + }>(CREATE_PROJECT_MUTATION, { input }); + + if (!result.projectCreate.success) { + throw new Error(`Failed to create project "${args.name}"`); + } + + return this.transformProject(result.projectCreate.project); + } + + /** + * List projects with optional filtering + * + * @param options Filter and pagination options + * @returns Array of projects + */ + async listProjects(options?: { + teamId?: string; + limit?: number; + }): Promise { + const filter: Record = {}; + + if (options?.teamId) { + const resolvedTeamId = this.isUUID(options.teamId) + ? options.teamId + : await this.linearService.resolveTeamId(options.teamId); + + if (!resolvedTeamId) { + throw new Error(`Team not found: "${options.teamId}"`); + } + + filter.accessibleTeams = { some: { id: { eq: resolvedTeamId } } }; + } + + const result = await this.graphqlService.rawRequest<{ + projects: { nodes: GraphQLProjectResponse[] }; + }>(LIST_PROJECTS_QUERY, { + first: options?.limit || 100, + filter: Object.keys(filter).length > 0 ? filter : undefined, + }); + + return result.projects.nodes.map((p) => this.transformProject(p)); + } +} diff --git a/src/utils/linear-service.ts b/src/utils/linear-service.ts index 654f2ac..23dc5a9 100644 --- a/src/utils/linear-service.ts +++ b/src/utils/linear-service.ts @@ -220,6 +220,9 @@ export class LinearService { targetDate: project.targetDate ? new Date(project.targetDate).toISOString() : undefined, + startDate: project.startDate + ? new Date(project.startDate).toISOString() + : undefined, createdAt: project.createdAt ? new Date(project.createdAt).toISOString() : new Date().toISOString(), diff --git a/src/utils/linear-types.d.ts b/src/utils/linear-types.d.ts index ec24d51..21429e7 100644 --- a/src/utils/linear-types.d.ts +++ b/src/utils/linear-types.d.ts @@ -87,6 +87,7 @@ export interface LinearProject { name: string; }; targetDate?: string; + startDate?: string; createdAt: string; updatedAt: string; } @@ -104,6 +105,7 @@ export interface CreateIssueArgs { parentId?: string; milestoneId?: string; cycleId?: string; + sortOrder?: number; } export interface UpdateIssueArgs { @@ -119,6 +121,7 @@ export interface UpdateIssueArgs { parentId?: string; milestoneId?: string | null; cycleId?: string | null; + sortOrder?: number; } export interface SearchIssuesArgs { @@ -130,6 +133,18 @@ export interface SearchIssuesArgs { limit?: number; } +export interface ProjectCreateArgs { + name: string; + teamIds: string[]; // Required - at least one team + description?: string; + color?: string; + icon?: string; + leadId?: string; + targetDate?: string; // ISO date string + startDate?: string; // ISO date string + state?: string; // "planned" | "started" | "paused" | "completed" | "canceled" +} + export interface LinearLabel { id: string; name: string; diff --git a/tests/unit/graphql-projects-service.test.ts b/tests/unit/graphql-projects-service.test.ts new file mode 100644 index 0000000..2914fa5 --- /dev/null +++ b/tests/unit/graphql-projects-service.test.ts @@ -0,0 +1,298 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { GraphQLProjectsService } from "../../src/utils/graphql-projects-service.js"; +import type { GraphQLService } from "../../src/utils/graphql-service.js"; +import type { LinearService } from "../../src/utils/linear-service.js"; + +/** + * Unit tests for GraphQLProjectsService + * + * Tests project creation with team resolution and validation. + */ + +describe("GraphQLProjectsService", () => { + let mockGraphQLService: { + rawRequest: ReturnType; + }; + let mockLinearService: { + resolveTeamId: ReturnType; + }; + let service: GraphQLProjectsService; + + beforeEach(() => { + mockGraphQLService = { + rawRequest: vi.fn(), + }; + mockLinearService = { + resolveTeamId: vi.fn(), + }; + + service = new GraphQLProjectsService( + mockGraphQLService as unknown as GraphQLService, + mockLinearService as unknown as LinearService, + ); + }); + + describe("createProject", () => { + it("should create project with resolved team ID", async () => { + // Setup: team resolution returns UUID + mockLinearService.resolveTeamId.mockResolvedValue("team-uuid-123"); + + // Setup: GraphQL returns success + mockGraphQLService.rawRequest.mockResolvedValue({ + projectCreate: { + success: true, + project: { + id: "project-uuid-456", + name: "Test Project", + description: null, + state: "planned", + progress: 0, + teams: { nodes: [{ id: "team-uuid-123", key: "ENG", name: "Engineering" }] }, + lead: null, + targetDate: null, + startDate: null, + createdAt: "2025-01-01T00:00:00Z", + updatedAt: "2025-01-01T00:00:00Z", + }, + }, + }); + + const result = await service.createProject({ + name: "Test Project", + teamIds: ["ENG"], + }); + + expect(result.id).toBe("project-uuid-456"); + expect(result.name).toBe("Test Project"); + expect(mockLinearService.resolveTeamId).toHaveBeenCalledWith("ENG"); + }); + + it("should pass UUID team IDs directly without resolution", async () => { + const teamUuid = "550e8400-e29b-41d4-a716-446655440000"; + + mockGraphQLService.rawRequest.mockResolvedValue({ + projectCreate: { + success: true, + project: { + id: "project-uuid-456", + name: "Test Project", + description: null, + state: "planned", + progress: 0, + teams: { nodes: [{ id: teamUuid, key: "ENG", name: "Engineering" }] }, + lead: null, + targetDate: null, + startDate: null, + createdAt: "2025-01-01T00:00:00Z", + updatedAt: "2025-01-01T00:00:00Z", + }, + }, + }); + + await service.createProject({ + name: "Test Project", + teamIds: [teamUuid], + }); + + // Should NOT call resolveTeamId for UUIDs + expect(mockLinearService.resolveTeamId).not.toHaveBeenCalled(); + + // Should pass UUID directly to GraphQL + const graphqlCall = mockGraphQLService.rawRequest.mock.calls[0]; + expect(graphqlCall[1].input.teamIds).toContain(teamUuid); + }); + + it("should throw error when team not found", async () => { + mockLinearService.resolveTeamId.mockResolvedValue(null); + + await expect( + service.createProject({ + name: "Test Project", + teamIds: ["NONEXISTENT"], + }), + ).rejects.toThrow('Team not found: "NONEXISTENT"'); + }); + + it("should validate leadId is a UUID", async () => { + await expect( + service.createProject({ + name: "Test Project", + teamIds: ["550e8400-e29b-41d4-a716-446655440000"], + leadId: "john.doe", // Not a UUID + }), + ).rejects.toThrow('Lead ID must be a UUID (got "john.doe")'); + }); + + it("should accept valid UUID for leadId", async () => { + const leadUuid = "660e8400-e29b-41d4-a716-446655440001"; + + mockGraphQLService.rawRequest.mockResolvedValue({ + projectCreate: { + success: true, + project: { + id: "project-uuid-456", + name: "Test Project", + description: null, + state: "planned", + progress: 0, + teams: { nodes: [] }, + lead: { id: leadUuid, name: "John Doe" }, + targetDate: null, + startDate: null, + createdAt: "2025-01-01T00:00:00Z", + updatedAt: "2025-01-01T00:00:00Z", + }, + }, + }); + + const result = await service.createProject({ + name: "Test Project", + teamIds: ["550e8400-e29b-41d4-a716-446655440000"], + leadId: leadUuid, + }); + + expect(result.lead?.id).toBe(leadUuid); + + // Verify leadId was passed to GraphQL + const graphqlCall = mockGraphQLService.rawRequest.mock.calls[0]; + expect(graphqlCall[1].input.leadId).toBe(leadUuid); + }); + + it("should include all optional fields in GraphQL input", async () => { + mockGraphQLService.rawRequest.mockResolvedValue({ + projectCreate: { + success: true, + project: { + id: "project-uuid-456", + name: "Full Project", + description: "A complete project", + state: "started", + progress: 0, + teams: { nodes: [] }, + lead: null, + targetDate: "2025-12-31", + startDate: "2025-01-15", + createdAt: "2025-01-01T00:00:00Z", + updatedAt: "2025-01-01T00:00:00Z", + }, + }, + }); + + await service.createProject({ + name: "Full Project", + teamIds: ["550e8400-e29b-41d4-a716-446655440000"], + description: "A complete project", + color: "#FF0000", + icon: "rocket", + targetDate: "2025-12-31", + startDate: "2025-01-15", + state: "started", + }); + + const graphqlCall = mockGraphQLService.rawRequest.mock.calls[0]; + const input = graphqlCall[1].input; + + expect(input.name).toBe("Full Project"); + expect(input.description).toBe("A complete project"); + expect(input.color).toBe("#FF0000"); + expect(input.icon).toBe("rocket"); + expect(input.targetDate).toBe("2025-12-31"); + expect(input.startDate).toBe("2025-01-15"); + expect(input.state).toBe("started"); + }); + + it("should throw error when GraphQL returns failure", async () => { + mockGraphQLService.rawRequest.mockResolvedValue({ + projectCreate: { + success: false, + project: null, + }, + }); + + await expect( + service.createProject({ + name: "Failing Project", + teamIds: ["550e8400-e29b-41d4-a716-446655440000"], + }), + ).rejects.toThrow('Failed to create project "Failing Project"'); + }); + + it("should include startDate in transformed response", async () => { + mockGraphQLService.rawRequest.mockResolvedValue({ + projectCreate: { + success: true, + project: { + id: "project-uuid-456", + name: "Test Project", + description: null, + state: "planned", + progress: 0, + teams: { nodes: [] }, + lead: null, + targetDate: "2025-12-31", + startDate: "2025-01-15", + createdAt: "2025-01-01T00:00:00Z", + updatedAt: "2025-01-01T00:00:00Z", + }, + }, + }); + + const result = await service.createProject({ + name: "Test Project", + teamIds: ["550e8400-e29b-41d4-a716-446655440000"], + }); + + // Verify startDate is included in output (Wolf fix verification) + expect(result.startDate).toBe("2025-01-15"); + expect(result.targetDate).toBe("2025-12-31"); + }); + }); + + describe("listProjects", () => { + it("should list projects without filter", async () => { + mockGraphQLService.rawRequest.mockResolvedValue({ + projects: { + nodes: [ + { + id: "project-1", + name: "Project A", + description: null, + state: "started", + progress: 50, + teams: { nodes: [{ id: "team-1", key: "ENG", name: "Engineering" }] }, + lead: null, + targetDate: null, + startDate: null, + createdAt: "2025-01-01T00:00:00Z", + updatedAt: "2025-01-01T00:00:00Z", + }, + ], + }, + }); + + const result = await service.listProjects(); + + expect(result).toHaveLength(1); + expect(result[0].name).toBe("Project A"); + }); + + it("should filter by team ID", async () => { + mockLinearService.resolveTeamId.mockResolvedValue("team-uuid-123"); + + mockGraphQLService.rawRequest.mockResolvedValue({ + projects: { + nodes: [], + }, + }); + + await service.listProjects({ teamId: "ENG" }); + + expect(mockLinearService.resolveTeamId).toHaveBeenCalledWith("ENG"); + + const graphqlCall = mockGraphQLService.rawRequest.mock.calls[0]; + expect(graphqlCall[1].filter).toEqual({ + accessibleTeams: { some: { id: { eq: "team-uuid-123" } } }, + }); + }); + }); +});