Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
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