Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ const project = await client.projects.create("workspace-slug", {
- **Labels**: Issue categorization and tagging
- **States**: Workflow state management
- **Users**: User management and profiles
- **Members**: Team membership and permissions
- **Roles**: Workspace and project role definitions (read-only)
- **Estimates**: Project estimates and estimate points
- **Modules**: Feature organization and module management
- **Cycles**: Sprint and iteration management
- **Customers**: Customer management and operations
Expand All @@ -82,7 +83,8 @@ const project = await client.projects.create("workspace-slug", {
- **WorkspaceProjectLabels**: Workspace-level project label management
- **WorkspaceProjectStates**: Workspace-level project state management
- **WorkItemRelationDefinitions**: Custom work item relation type definitions
- **Releases**: Release management with tags, labels, and item label assignment
- **Releases**: Release management with tags, labels, item labels, changelog, comments, links, and work items
- **Collections**: Folders that group workspace pages, with member and page management
- **AgentRuns**: AI agent run orchestration and activity tracking
- **Workflows**: Project workflow management with state attachments and transitions
- **ProjectTemplates**: Work item and page template management per project
Expand Down
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module.exports = {
preset: "ts-jest",
setupFiles: ["dotenv/config"],
testEnvironment: "node",
roots: ["<rootDir>/tests"],
testMatch: ["**/__tests__/**/*.ts", "**/?(*.)+(spec|test).ts"],
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@makeplane/plane-node-sdk",
"version": "0.2.12",
"version": "0.2.13",
"description": "Node SDK for Plane",
"author": "Plane <engineering@plane.so>",
"repository": {
Expand Down Expand Up @@ -44,6 +44,7 @@
"devDependencies": {
"@types/jest": "^29.0.0",
"@types/node": "^20.0.0",
"dotenv": "^17.4.2",
"dts-bundle-generator": "^9.5.1",
"jest": "^29.0.0",
"oxfmt": "^0.42.0",
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 17 additions & 2 deletions src/api/BaseResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,12 @@ export abstract class BaseResource {
/**
* DELETE request
*/
protected async httpDelete(endpoint: string, data?: any): Promise<void> {
protected async httpDelete(endpoint: string, data?: any, params?: any): Promise<void> {
try {
await axios.delete(this.buildUrl(endpoint), {
headers: this.getHeaders(),
data,
params,
});
} catch (error) {
throw this.handleError(error);
Expand Down Expand Up @@ -154,7 +155,21 @@ export abstract class BaseResource {
* Centralized error handling
*/
protected handleError(error: any): never {
console.error("❌ [ERROR]", error);
if (this.config.enableLogging) {
if (axios.isAxiosError(error)) {
console.error("❌ [ERROR]", {
method: error.config?.method?.toUpperCase(),
url: error.config?.url,
status: error.response?.status,
headers: this.sanitizeHeaders(error.config?.headers),
requestData: error.config?.data ? this.sanitizeData(error.config.data) : undefined,
responseData: this.sanitizeData(error.response?.data),
message: error.message,
});
} else {
console.error("❌ [ERROR]", error instanceof Error ? error.message : error);
}
}

if (error instanceof HttpError) {
throw error;
Expand Down
60 changes: 60 additions & 0 deletions src/api/Collections/Members.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { BaseResource } from "../BaseResource";
import { Configuration } from "../../Configuration";
import { CollectionMember, CreateCollectionMember, UpdateCollectionMember } from "../../models/Collection";

/**
* CollectionMembers sub-resource
* Manages members of a (typically private) collection
*/
export class Members extends BaseResource {
constructor(config: Configuration) {
super(config);
}

/**
* List members of a collection
*/
async list(workspaceSlug: string, collectionId: string): Promise<CollectionMember[]> {
const data = await this.get<CollectionMember[] | { results: CollectionMember[] }>(
`/workspaces/${workspaceSlug}/collections/${collectionId}/members/`
);
return Array.isArray(data) ? data : data.results;
}

/**
* Add a member to a collection
*/
async add(
workspaceSlug: string,
collectionId: string,
memberData: CreateCollectionMember
): Promise<CollectionMember> {
return this.post<CollectionMember>(`/workspaces/${workspaceSlug}/collections/${collectionId}/members/`, memberData);
}

/**
* Update a collection member's access level.
*
* @param memberId - UUID of the CollectionMember row (not the user id)
*/
async update(
workspaceSlug: string,
collectionId: string,
memberId: string,
memberData: UpdateCollectionMember
): Promise<CollectionMember> {
return this.patch<CollectionMember>(
`/workspaces/${workspaceSlug}/collections/${collectionId}/members/${memberId}/`,
memberData
);
}

/**
* Remove a member from a collection.
*
* @param memberId - UUID of the CollectionMember row (not the user id)
*/
async remove(workspaceSlug: string, collectionId: string, memberId: string): Promise<void> {
return this.httpDelete(`/workspaces/${workspaceSlug}/collections/${collectionId}/members/${memberId}/`);
}
}
87 changes: 87 additions & 0 deletions src/api/Collections/Pages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { BaseResource } from "../BaseResource";
import { Configuration } from "../../Configuration";
import { PaginatedResponse } from "../../models/common";
import {
AddCollectionPages,
CollectionBranchPage,
CollectionPage,
CollectionPageSearchResult,
ListCollectionPagesParams,
UpdateCollectionPage,
} from "../../models/Collection";

/**
* CollectionPages sub-resource
* Manages the pages that belong to a collection
*/
export class Pages extends BaseResource {
constructor(config: Configuration) {
super(config);
}

/**
* List pages that belong to a collection.
* Without `parent_id`, returns only top-level (non-sub) pages.
*/
async list(
workspaceSlug: string,
collectionId: string,
params?: ListCollectionPagesParams
): Promise<PaginatedResponse<CollectionBranchPage>> {
return this.get<PaginatedResponse<CollectionBranchPage>>(
`/workspaces/${workspaceSlug}/collections/${collectionId}/pages/`,
params
);
}

/**
* Add existing page(s) to a collection
*/
async add(workspaceSlug: string, collectionId: string, pagesData: AddCollectionPages): Promise<CollectionPage[]> {
const result = await this.post<CollectionPage[] | { results: CollectionPage[] }>(
`/workspaces/${workspaceSlug}/collections/${collectionId}/pages/`,
pagesData
);
return Array.isArray(result) ? result : result.results;
}

/**
* Search pages that are not yet in a collection, to add them.
*
* @param search - Optional case-insensitive substring filter on page name
*/
async search(workspaceSlug: string, collectionId: string, search?: string): Promise<CollectionPageSearchResult[]> {
const data = await this.get<CollectionPageSearchResult[] | { results: CollectionPageSearchResult[] }>(
`/workspaces/${workspaceSlug}/collections/${collectionId}/pages-search/`,
search ? { search } : undefined
);
return Array.isArray(data) ? data : data.results;
}

/**
* Move a page to a different collection, or reorder it within the current one.
* Omit `collection` in the payload to just reorder.
*
* @param pageCollectionId - UUID of the page-collection membership row
*/
async update(
workspaceSlug: string,
collectionId: string,
pageCollectionId: string,
updateData: UpdateCollectionPage
): Promise<CollectionPage> {
return this.patch<CollectionPage>(
`/workspaces/${workspaceSlug}/collections/${collectionId}/pages/${pageCollectionId}/`,
updateData
);
}

/**
* Remove a page from a collection (does not delete the page itself).
*
* @param pageCollectionId - UUID of the page-collection membership row
*/
async remove(workspaceSlug: string, collectionId: string, pageCollectionId: string): Promise<void> {
return this.httpDelete(`/workspaces/${workspaceSlug}/collections/${collectionId}/pages/${pageCollectionId}/`);
}
}
63 changes: 63 additions & 0 deletions src/api/Collections/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { BaseResource } from "../BaseResource";
import { Configuration } from "../../Configuration";
import { Collection, CreateCollection, UpdateCollection } from "../../models/Collection";
import { Members } from "./Members";
import { Pages } from "./Pages";

/**
* Collections API resource
* Manages collections (folders that group workspace pages) with member and
* page sub-resources
*/
export class Collections extends BaseResource {
public members: Members;
public pages: Pages;

constructor(config: Configuration) {
super(config);
this.members = new Members(config);
this.pages = new Pages(config);
}

/**
* List all collections in a workspace
*/
async list(workspaceSlug: string): Promise<Collection[]> {
const data = await this.get<Collection[] | { results: Collection[] }>(`/workspaces/${workspaceSlug}/collections/`);
return Array.isArray(data) ? data : data.results;
}

/**
* Create a new collection in a workspace
*/
async create(workspaceSlug: string, createCollection: CreateCollection): Promise<Collection> {
return this.post<Collection>(`/workspaces/${workspaceSlug}/collections/`, createCollection);
}

/**
* Retrieve a collection by ID
*/
async retrieve(workspaceSlug: string, collectionId: string): Promise<Collection> {
return this.get<Collection>(`/workspaces/${workspaceSlug}/collections/${collectionId}/`);
}

/**
* Update a collection's name, logo, or sort order.
* A collection's access level cannot be changed after creation.
*/
async update(workspaceSlug: string, collectionId: string, updateCollection: UpdateCollection): Promise<Collection> {
return this.patch<Collection>(`/workspaces/${workspaceSlug}/collections/${collectionId}/`, updateCollection);
}

/**
* Delete a collection.
*
* @param archivePages - Whether to archive the collection's pages instead of
* leaving them unfiled. Omit to use the server's default (true). Private
* collections always archive their pages regardless.
*/
async delete(workspaceSlug: string, collectionId: string, archivePages?: boolean): Promise<void> {
const params = archivePages === undefined ? undefined : { archive_pages: archivePages ? "true" : "false" };
return this.httpDelete(`/workspaces/${workspaceSlug}/collections/${collectionId}/`, undefined, params);
}
Comment thread
akhil-vamshi-konam marked this conversation as resolved.
}
13 changes: 13 additions & 0 deletions src/api/Customers/Properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
CreateCustomerPropertyRequest,
UpdateCustomerPropertyRequest,
CustomPropertyValueResponse,
SetCustomerPropertyValues,
} from "../../models/Customer";
import { PaginatedResponse } from "../../models/common";

Expand Down Expand Up @@ -87,6 +88,18 @@ export class Properties extends BaseResource {
);
}

/**
* Set several of a customer's property values at once.
*
* Acts as an upsert: the properties named are given these values, replacing
* any they already held. Properties absent from the payload are untouched.
* Every value is sent as a string — dates as YYYY-MM-DD, booleans as
* "True"/"False", options and relations as UUIDs.
*/
async createValues(workspaceSlug: string, customerId: string, values: SetCustomerPropertyValues): Promise<void> {
await this.post<void>(`/workspaces/${workspaceSlug}/customers/${customerId}/property-values/`, values);
}

/**
* Get single property value
*/
Expand Down
12 changes: 12 additions & 0 deletions src/api/Customers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ export class Customers extends BaseResource {
return this.httpDelete(`/workspaces/${workspaceSlug}/customers/${customerId}/`);
}

/**
* Delete a customer addressed by its external reference
* (external_source + external_id query params) instead of its id.
* Deleting by an external reference that matches nothing succeeds silently.
*/
async deleteByExternalId(workspaceSlug: string, externalSource: string, externalId: string): Promise<void> {
return this.httpDelete(`/workspaces/${workspaceSlug}/customers/`, undefined, {
external_source: externalSource,
external_id: externalId,
});
Comment thread
akhil-vamshi-konam marked this conversation as resolved.
}

/**
* List customers with optional filtering
*/
Expand Down
Loading
Loading