-
Notifications
You must be signed in to change notification settings - Fork 4
chore: adding milestone models and resources #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| import { BaseResource } from "./BaseResource"; | ||
| import { Configuration } from "../Configuration"; | ||
| import { PaginatedResponse } from "../models/common"; | ||
|
|
||
| import { Milestone, CreateMilestoneRequest, UpdateMilestoneRequest, MilestoneWorkItem } from "../models/Milestone"; | ||
|
|
||
| /** | ||
| * Milestones API resource | ||
| * Handles all milestone related operations | ||
| */ | ||
| export class Milestones extends BaseResource { | ||
| constructor(config: Configuration) { | ||
| super(config); | ||
| } | ||
|
|
||
| /** | ||
| * Create a new milestone | ||
| */ | ||
| async create(workspaceSlug: string, projectId: string, createMilestone: CreateMilestoneRequest): Promise<Milestone> { | ||
| return this.post<Milestone>(`/workspaces/${workspaceSlug}/projects/${projectId}/milestones/`, createMilestone); | ||
| } | ||
|
|
||
| /** | ||
| * Retrieve a milestone by ID | ||
| */ | ||
| async retrieve(workspaceSlug: string, projectId: string, milestoneId: string): Promise<Milestone> { | ||
| return this.get<Milestone>(`/workspaces/${workspaceSlug}/projects/${projectId}/milestones/${milestoneId}/`); | ||
| } | ||
|
|
||
| /** | ||
| * Update a milestone | ||
| */ | ||
| async update( | ||
| workspaceSlug: string, | ||
| projectId: string, | ||
| milestoneId: string, | ||
| updateMilestone: UpdateMilestoneRequest | ||
| ): Promise<Milestone> { | ||
| return this.patch<Milestone>( | ||
| `/workspaces/${workspaceSlug}/projects/${projectId}/milestones/${milestoneId}/`, | ||
| updateMilestone | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Delete a milestone | ||
| */ | ||
| async delete(workspaceSlug: string, projectId: string, milestoneId: string): Promise<void> { | ||
| return this.httpDelete(`/workspaces/${workspaceSlug}/projects/${projectId}/milestones/${milestoneId}/`); | ||
| } | ||
|
|
||
| /** | ||
| * List milestones with optional filtering | ||
| */ | ||
| async list(workspaceSlug: string, projectId: string, params?: any): Promise<PaginatedResponse<Milestone>> { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Replace
♻️ Proposed fix- async list(workspaceSlug: string, projectId: string, params?: any): Promise<PaginatedResponse<Milestone>> {
+ async list(workspaceSlug: string, projectId: string, params?: Record<string, unknown>): Promise<PaginatedResponse<Milestone>> {- params?: any
+ params?: Record<string, unknown>As per coding guidelines, "Avoid Also applies to: 92-92 🤖 Prompt for AI Agents |
||
| return this.get<PaginatedResponse<Milestone>>( | ||
| `/workspaces/${workspaceSlug}/projects/${projectId}/milestones/`, | ||
| params | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Add work items to a milestone | ||
| */ | ||
| async addWorkItems(workspaceSlug: string, projectId: string, milestoneId: string, issueIds: string[]): Promise<void> { | ||
| return this.post<void>(`/workspaces/${workspaceSlug}/projects/${projectId}/milestones/${milestoneId}/work-items/`, { | ||
| issues: issueIds, | ||
| }); | ||
|
Comment on lines
+65
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n src/api/Milestones.ts | head -90Repository: makeplane/plane-node-sdk Length of output: 3413 🏁 Script executed: # Check if "issues" is a backend API contract by looking at other API usages and responses
rg -n "issues" --type=ts src/api/ -B 2 -A 2 | head -100Repository: makeplane/plane-node-sdk Length of output: 5317 🏁 Script executed: # Check if there are any type definitions or models that define the payload structure
fd -e ts -e json . src/models src/types 2>/dev/null | head -20Repository: makeplane/plane-node-sdk Length of output: 515 🏁 Script executed: # Check Modules.ts to see the full pattern of how payload is structured
sed -n '75,90p' src/api/Modules.tsRepository: makeplane/plane-node-sdk Length of output: 456 🏁 Script executed: # Check Cycles.ts to see the same pattern
sed -n '100,110p' src/api/Cycles.tsRepository: makeplane/plane-node-sdk Length of output: 346 🏁 Script executed: # Search for any backend response or test fixtures that might show the expected payload structure
rg -n "work_items|work-items" --type=ts src/ | head -20Repository: makeplane/plane-node-sdk Length of output: 2863 Rename "issue" parameter to "work item" terminology; keep backend API payload contract. The parameter However, the payload key ♻️ Proposed fix- async addWorkItems(workspaceSlug: string, projectId: string, milestoneId: string, issueIds: string[]): Promise<void> {
+ async addWorkItems(workspaceSlug: string, projectId: string, milestoneId: string, workItemIds: string[]): Promise<void> {
return this.post<void>(`/workspaces/${workspaceSlug}/projects/${projectId}/milestones/${milestoneId}/work-items/`, {
- issues: issueIds,
+ issues: workItemIds,
});
} async removeWorkItems(
workspaceSlug: string,
projectId: string,
milestoneId: string,
- issueIds: string[]
+ workItemIds: string[]
): Promise<void> {
return this.httpDelete(`/workspaces/${workspaceSlug}/projects/${projectId}/milestones/${milestoneId}/work-items/`, {
- issues: issueIds,
+ issues: workItemIds,
});
}Also applies to: 74-82 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| /** | ||
| * Remove work items from a milestone | ||
| */ | ||
| async removeWorkItems( | ||
| workspaceSlug: string, | ||
| projectId: string, | ||
| milestoneId: string, | ||
| issueIds: string[] | ||
| ): Promise<void> { | ||
| return this.httpDelete(`/workspaces/${workspaceSlug}/projects/${projectId}/milestones/${milestoneId}/work-items/`, { | ||
| issues: issueIds, | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * List work items in a milestone | ||
| */ | ||
| async listWorkItems( | ||
| workspaceSlug: string, | ||
| projectId: string, | ||
| milestoneId: string, | ||
| params?: any | ||
| ): Promise<PaginatedResponse<MilestoneWorkItem>> { | ||
| return this.get<PaginatedResponse<MilestoneWorkItem>>( | ||
| `/workspaces/${workspaceSlug}/projects/${projectId}/milestones/${milestoneId}/work-items/`, | ||
| params | ||
| ); | ||
| } | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import { BaseModel } from "./common"; | ||
|
|
||
| export interface Milestone extends BaseModel { | ||
| title: string; | ||
| // YYYY-MM-DD format | ||
| target_date?: string; | ||
| project: string; | ||
| workspace: string; | ||
| } | ||
|
|
||
| export interface MilestoneLite { | ||
| id?: string; | ||
| title: string; | ||
| // YYYY-MM-DD format | ||
| target_date?: string; | ||
| external_source?: string; | ||
| external_id?: string; | ||
| created_at?: string; | ||
| updated_at?: string; | ||
| } | ||
|
|
||
| export interface CreateMilestoneRequest { | ||
| title: string; | ||
| // YYYY-MM-DD format | ||
| target_date?: string; | ||
| external_source?: string; | ||
| external_id?: string; | ||
| } | ||
|
|
||
| export interface UpdateMilestoneRequest { | ||
| title?: string; | ||
| // YYYY-MM-DD format | ||
| target_date?: string; | ||
| external_source?: string; | ||
| external_id?: string; | ||
| } | ||
|
|
||
| export interface MilestoneWorkItem { | ||
| id?: string; | ||
| issue?: string; | ||
| milestone?: string; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| import { PlaneClient } from "../../src/client/plane-client"; | ||
| import { Milestone, UpdateMilestoneRequest, MilestoneWorkItem } from "../../src/models"; | ||
| import { config } from "./constants"; | ||
| import { createTestClient } from "../helpers/test-utils"; | ||
| import { describeIf as describe } from "../helpers/conditional-tests"; | ||
|
|
||
| describe(!!(config.workspaceSlug && config.projectId && config.workItemId), "Milestone API Tests", () => { | ||
| let client: PlaneClient; | ||
| let workspaceSlug: string; | ||
| let projectId: string; | ||
| let workItemId: string; | ||
| let milestone: Milestone; | ||
|
|
||
| beforeAll(async () => { | ||
| client = createTestClient(); | ||
| workspaceSlug = config.workspaceSlug; | ||
| projectId = config.projectId; | ||
| workItemId = config.workItemId; | ||
| }); | ||
|
|
||
| afterAll(async () => { | ||
| // Clean up created resources | ||
| if (milestone?.id) { | ||
| try { | ||
| await client.milestones.delete(workspaceSlug, projectId, milestone.id); | ||
| } catch (error) { | ||
| console.warn("Failed to delete milestone:", error); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| it("should create a milestone", async () => { | ||
| milestone = await client.milestones.create(workspaceSlug, projectId, { | ||
| title: "Test Milestone", | ||
| }); | ||
|
|
||
| expect(milestone).toBeDefined(); | ||
| expect(milestone.id).toBeDefined(); | ||
| expect(milestone.title).toBe("Test Milestone"); | ||
| }); | ||
|
|
||
| it("should retrieve a milestone", async () => { | ||
| const retrievedMilestone = await client.milestones.retrieve(workspaceSlug, projectId, milestone.id); | ||
|
|
||
| expect(retrievedMilestone).toBeDefined(); | ||
| expect(retrievedMilestone.id).toBe(milestone.id); | ||
| expect(retrievedMilestone.title).toBe(milestone.title); | ||
| }); | ||
|
|
||
| it("should update a milestone", async () => { | ||
| const updateData: UpdateMilestoneRequest = { | ||
| title: "Updated Test Milestone", | ||
| target_date: "2026-12-31", | ||
| }; | ||
|
|
||
| const updatedMilestone = await client.milestones.update( | ||
| workspaceSlug, | ||
| projectId, | ||
| milestone.id, | ||
| updateData | ||
| ); | ||
|
|
||
| expect(updatedMilestone).toBeDefined(); | ||
| expect(updatedMilestone.id).toBe(milestone.id); | ||
| expect(updatedMilestone.title).toBe("Updated Test Milestone"); | ||
| expect(updatedMilestone.target_date).toBe("2026-12-31"); | ||
| }); | ||
|
|
||
| it("should list milestones", async () => { | ||
| const milestones = await client.milestones.list(workspaceSlug, projectId); | ||
|
|
||
| expect(milestones).toBeDefined(); | ||
| expect(Array.isArray(milestones.results)).toBe(true); | ||
| expect(milestones.results.length).toBeGreaterThan(0); | ||
|
|
||
| const foundMilestone = milestones.results.find((m) => m.id === milestone.id); | ||
| expect(foundMilestone).toBeDefined(); | ||
| expect(foundMilestone?.title).toBe("Updated Test Milestone"); | ||
| }); | ||
|
|
||
| it("should add work items to milestone", async () => { | ||
| await client.milestones.addWorkItems(workspaceSlug, projectId, milestone.id, [workItemId]); | ||
|
|
||
| const workItems = await client.milestones.listWorkItems(workspaceSlug, projectId, milestone.id); | ||
|
|
||
| expect(workItems).toBeDefined(); | ||
| expect(Array.isArray(workItems.results)).toBe(true); | ||
| expect(workItems.results.length).toBeGreaterThan(0); | ||
| }); | ||
|
|
||
| it("should list work items in milestone", async () => { | ||
| const workItems = await client.milestones.listWorkItems(workspaceSlug, projectId, milestone.id); | ||
|
|
||
| expect(workItems).toBeDefined(); | ||
| expect(Array.isArray(workItems.results)).toBe(true); | ||
| expect(workItems.results.length).toBeGreaterThan(0); | ||
| }); | ||
|
|
||
| it("should remove work items from milestone", async () => { | ||
| await client.milestones.removeWorkItems(workspaceSlug, projectId, milestone.id, [workItemId]); | ||
|
|
||
| const workItems = await client.milestones.listWorkItems(workspaceSlug, projectId, milestone.id); | ||
|
|
||
| expect(workItems).toBeDefined(); | ||
| expect(Array.isArray(workItems.results)).toBe(true); | ||
|
|
||
| const foundWorkItem = workItems.results.find((item: MilestoneWorkItem) => item.issue === workItemId); | ||
| expect(foundWorkItem).toBeUndefined(); | ||
| }); | ||
|
|
||
| it("should delete a milestone", async () => { | ||
| await client.milestones.delete(workspaceSlug, projectId, milestone.id); | ||
|
|
||
| // Verify it's deleted by trying to retrieve it | ||
| try { | ||
| await client.milestones.retrieve(workspaceSlug, projectId, milestone.id); | ||
| fail("Expected an error when retrieving a deleted milestone"); | ||
| } catch (error) { | ||
| expect(error).toBeDefined(); | ||
| } | ||
|
|
||
| // Prevent afterAll from trying to delete again | ||
| milestone = undefined as any; | ||
| }); | ||
|
Comment on lines
+111
to
+124
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🐛 Proposed fix try {
await client.milestones.retrieve(workspaceSlug, projectId, milestone.id);
- fail("Expected an error when retrieving a deleted milestone");
+ throw new Error("Expected an error when retrieving a deleted milestone");
} catch (error) {
- expect(error).toBeDefined();
+ expect(error).toBeDefined();
+ // Ensure we didn't catch our own thrown error
+ expect((error as Error).message).not.toBe("Expected an error when retrieving a deleted milestone");
}🧰 Tools🪛 ESLint[error] 111-111: 'it' is not defined. (no-undef) [error] 117-117: 'fail' is not defined. (no-undef) [error] 119-119: 'expect' is not defined. (no-undef) 🤖 Prompt for AI Agents |
||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Rename
deletetodelto match the standard resource method pattern.Per coding guidelines, standard resource methods should follow the pattern:
list,create,retrieve,update,del. As per coding guidelines, "Standard resource methods should follow the pattern:list,create,retrieve,update,del".♻️ Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents