From 0b9e1f5c30460d01ea93db66149aaef600903bcd Mon Sep 17 00:00:00 2001 From: Andrey Strinski Date: Thu, 11 Jun 2026 10:07:09 +0300 Subject: [PATCH 01/11] Add support for the path endpoints --- lib/folders/index.ts | 22 +++ lib/folders/types.ts | 82 +++++++++ lib/reports/index.ts | 22 +++ lib/reports/types.ts | 92 +++++++++ lib/sheets/index.js | 17 ++ lib/sheets/types.ts | 63 +++++++ lib/sights/index.ts | 22 +++ lib/sights/types.ts | 92 +++++++++ .../mock-api/folders/common_test_constants.ts | 6 + test/mock-api/folders/get_folder_path.spec.ts | 165 +++++++++++++++++ .../mock-api/reports/common_test_constants.ts | 10 + test/mock-api/reports/get_report_path.spec.ts | 174 ++++++++++++++++++ test/mock-api/sheets/common_test_constants.ts | 16 ++ test/mock-api/sheets/get_sheet_path.spec.ts | 174 ++++++++++++++++++ test/mock-api/sights/common_test_constants.ts | 16 ++ test/mock-api/sights/get_sight_path.spec.ts | 174 ++++++++++++++++++ 16 files changed, 1147 insertions(+) create mode 100644 test/mock-api/folders/get_folder_path.spec.ts create mode 100644 test/mock-api/reports/get_report_path.spec.ts create mode 100644 test/mock-api/sheets/common_test_constants.ts create mode 100644 test/mock-api/sheets/get_sheet_path.spec.ts create mode 100644 test/mock-api/sights/common_test_constants.ts create mode 100644 test/mock-api/sights/get_sight_path.spec.ts diff --git a/lib/folders/index.ts b/lib/folders/index.ts index 1e7c6750..0aa8d705 100644 --- a/lib/folders/index.ts +++ b/lib/folders/index.ts @@ -1,3 +1,4 @@ +import type { ApiError } from '../types/ApiError'; import type { CreateOptions } from '../types/CreateOptions'; import type { RequestCallback } from '../types/RequestCallback'; import type { BaseResponseStatus } from '../types/BaseResponseStatus'; @@ -16,7 +17,9 @@ import type { MoveFolderOptions, MoveFolderResponse, CopyFolderResponse, + GetFolderPathOptions, } from './types'; +import { FolderPathNode } from './types'; export function create(options: CreateOptions): FoldersApi { const requestor = options.requestor; @@ -83,6 +86,24 @@ export function create(options: CreateOptions): FoldersApi { return requestor.get({ ...optionsToSend, ...urlOptions, ...getOptions }, callback); }; + const getFolderPath = ( + getOptions: GetFolderPathOptions, + callback?: RequestCallback + ): Promise => { + const urlOptions = { url: options.apiUrls.folders + '/' + getOptions.folderId + '/path' }; + return requestor + .get({ ...optionsToSend, ...urlOptions, ...getOptions }) + .then((data: Record) => { + const node = new FolderPathNode(data); + if (callback) callback(undefined, node); + return node; + }) + .catch((err: unknown) => { + if (callback) callback(err as ApiError, undefined); + return Promise.reject(err); + }); + }; + return { getFolderMetadata, getFolderChildren, @@ -91,5 +112,6 @@ export function create(options: CreateOptions): FoldersApi { deleteFolder, moveFolder, copyFolder, + getFolderPath, }; } diff --git a/lib/folders/types.ts b/lib/folders/types.ts index a5b4fafb..d2cbbf1e 100644 --- a/lib/folders/types.ts +++ b/lib/folders/types.ts @@ -196,6 +196,28 @@ export interface FoldersApi { options: MoveFolderOptions, callback?: RequestCallback ) => Promise; + + /** + * Gets the path from the workspace root to the specified folder. + * + * @param options - {@link GetFolderPathOptions} - Configuration options for the request + * @param callback - {@link RequestCallback}\<{@link FolderPathNode}\> - Optional callback function + * @returns Promise\<{@link FolderPathNode}\> + * + * @remarks + * It mirrors to the following Smartsheet REST API method: `GET /folders/{folderId}/path` + * + * @example + * ```typescript + * const path = await client.folders.getFolderPath({ + * folderId: 7116448184199044 + * }); + * ``` + */ + getFolderPath: ( + options: GetFolderPathOptions, + callback?: RequestCallback + ) => Promise; } // ============================================================================ @@ -716,3 +738,63 @@ export interface MoveFolderResponse extends BaseResponseStatus { */ result: Folder; } + +// ============================================================================ +// Get Folder Path +// ============================================================================ + +export interface PathLeaf { + id: number; + name?: string; + permalink?: string; + accessLevel?: string; + createdAt?: string; + modifiedAt?: string; +} + +export class FolderPathNode { + id: number; + name?: string; + permalink?: string; + accessLevel?: string; + folders?: FolderPathNode[]; + + constructor(data: Record) { + this.id = data.id as number; + this.name = data.name as string | undefined; + this.permalink = data.permalink as string | undefined; + this.accessLevel = data.accessLevel as string | undefined; + if (Array.isArray(data.folders)) { + this.folders = (data.folders as Record[]).map((f) => new FolderPathNode(f)); + } + } + + private _walkToLeaf(): FolderPathNode[] { + if (this.folders && this.folders.length > 0) { + return [this, ...this.folders[0]._walkToLeaf()]; + } + return [this]; + } + + /** Returns the deepest FolderPathNode (the target folder). */ + getFolder(): FolderPathNode { + const nodes = this._walkToLeaf(); + return nodes[nodes.length - 1]; + } + + /** Returns a slash-separated path string of folder names from this node to the target folder. */ + getFolderPath(): string { + const nodes = this._walkToLeaf(); + return nodes + .map((n) => n.name) + .filter(Boolean) + .join('/'); + } +} + +export interface GetFolderPathOptions extends RequestOptions { + /** + * Folder Id. + */ + folderId: number; +} diff --git a/lib/reports/index.ts b/lib/reports/index.ts index fe1b47ee..c600832b 100644 --- a/lib/reports/index.ts +++ b/lib/reports/index.ts @@ -1,3 +1,4 @@ +import type { ApiError } from '../types/ApiError'; import type { BaseResponseStatus } from '../types/BaseResponseStatus'; import type { CreateOptions } from '../types/CreateOptions'; import type { RequestCallback } from '../types/RequestCallback'; @@ -25,7 +26,9 @@ import type { AddReportColumnsResponse, CreateReportOptions, CreateReportResponse, + GetReportPathOptions, } from './types'; +import { ReportPathNode } from './types'; import * as constants from '../utils/constants'; export function create(options: CreateOptions): ReportsApi { @@ -140,6 +143,24 @@ export function create(options: CreateOptions): ReportsApi { return requestor.post({ ...optionsToSend, ...urlOptions, ...postOptions }, callback); }; + const getReportPath = ( + getOptions: GetReportPathOptions, + callback?: RequestCallback + ): Promise => { + const urlOptions = { url: options.apiUrls.reports + '/' + getOptions.reportId + '/path' }; + return requestor + .get({ ...optionsToSend, ...urlOptions, ...getOptions }) + .then((data: Record) => { + const node = new ReportPathNode(data); + if (callback) callback(undefined, node); + return node; + }) + .catch((err: unknown) => { + if (callback) callback(err as ApiError, undefined); + return Promise.reject(err); + }); + }; + return { listReports, getReport, @@ -154,5 +175,6 @@ export function create(options: CreateOptions): ReportsApi { removeReportScope, addReportColumns, createReport, + getReportPath, }; } diff --git a/lib/reports/types.ts b/lib/reports/types.ts index 380dc3e0..4e55dda1 100644 --- a/lib/reports/types.ts +++ b/lib/reports/types.ts @@ -430,6 +430,28 @@ export interface ReportsApi { options: CreateReportOptions, callback?: RequestCallback ) => Promise; + + /** + * Gets the path from the workspace root to the specified report. + * + * @param options - {@link GetReportPathOptions} - Configuration options for the request + * @param callback - {@link RequestCallback}\<{@link ReportPathNode}\> - Optional callback function + * @returns Promise\<{@link ReportPathNode}\> + * + * @remarks + * It mirrors to the following Smartsheet REST API method: `GET /reports/{reportId}/path` + * + * @example + * ```typescript + * const path = await client.reports.getReportPath({ + * reportId: 4583173393803140 + * }); + * ``` + */ + getReportPath: ( + options: GetReportPathOptions, + callback?: RequestCallback + ) => Promise; } // ============================================================================ @@ -1639,3 +1661,73 @@ export interface CreateReportResponse extends BaseResponseStatus { */ result: CreateReportResult; } + +// ============================================================================ +// Get Report Path +// ============================================================================ + +export interface PathLeaf { + id: number; + name?: string; + permalink?: string; + accessLevel?: string; + createdAt?: string; + modifiedAt?: string; +} + +export class ReportPathNode { + id: number; + name?: string; + permalink?: string; + accessLevel?: string; + folders?: ReportPathNode[]; + reports?: PathLeaf[]; + + constructor(data: Record) { + this.id = data.id as number; + this.name = data.name as string | undefined; + this.permalink = data.permalink as string | undefined; + this.accessLevel = data.accessLevel as string | undefined; + if (Array.isArray(data.folders)) { + this.folders = (data.folders as Record[]).map((f) => new ReportPathNode(f)); + } + if (Array.isArray(data.reports)) { + this.reports = data.reports as PathLeaf[]; + } + } + + private _walkToLeaf(): ReportPathNode[] { + if (this.reports && this.reports.length > 0) return [this]; + if (this.folders && this.folders.length > 0) { + return [this, ...this.folders[0]._walkToLeaf()]; + } + return [this]; + } + + /** Returns the target PathLeaf report, or undefined if not reachable. */ + getReport(): PathLeaf | undefined { + for (const node of this._walkToLeaf()) { + if (node.reports && node.reports.length > 0) return node.reports[0]; + } + return undefined; + } + + /** Returns a slash-separated path string from this node to the target report. */ + getReportPath(): string | undefined { + const nodes = this._walkToLeaf(); + if (nodes.length === 0) return undefined; + const parts = nodes.map((n) => n.name).filter(Boolean) as string[]; + const leaf = nodes[nodes.length - 1]; + if (leaf.reports && leaf.reports.length > 0 && leaf.reports[0].name) { + parts[parts.length - 1] = leaf.reports[0].name; + } + return parts.length > 0 ? parts.join('/') : undefined; + } +} + +export interface GetReportPathOptions extends RequestOptions { + /** + * Report Id. + */ + reportId: number; +} diff --git a/lib/sheets/index.js b/lib/sheets/index.js index e18394b6..10593c96 100644 --- a/lib/sheets/index.js +++ b/lib/sheets/index.js @@ -1,4 +1,5 @@ import _ from 'underscore'; +import { SheetPathNode } from './types'; import * as attachments from './attachments'; import * as automationRules from './automationrules'; import * as columns from './columns'; @@ -55,6 +56,21 @@ export function create(options) { return requestor.post(_.extend({}, optionsToSend, urlOptions, postOptions), callback); }; + const getSheetPath = (getOptions, callback) => { + const urlOptions = { url: options.apiUrls.sheets + '/' + getOptions.sheetId + '/path' }; + return requestor + .get(_.extend({}, optionsToSend, urlOptions, getOptions)) + .then((data) => { + const node = new SheetPathNode(data); + if (callback) callback(undefined, node); + return node; + }) + .catch((err) => { + if (callback) callback(err, undefined); + return Promise.reject(err); + }); + }; + const sheetObject = { sendSheetViaEmail: sendSheetViaEmail, getPublishStatus: getPublishStatus, @@ -63,6 +79,7 @@ export function create(options) { deleteSheet: deleteSheet, moveSheet: moveSheet, sortRowsInSheet: sortRowsInSheet, + getSheetPath: getSheetPath, }; _.extend(sheetObject, attachments.create(options)); diff --git a/lib/sheets/types.ts b/lib/sheets/types.ts index ccdaf79f..be70b667 100644 --- a/lib/sheets/types.ts +++ b/lib/sheets/types.ts @@ -1,3 +1,66 @@ +// ============================================================================ +// Get Sheet Path +// ============================================================================ + +export interface SheetPathLeaf { + id: number; + name?: string; + permalink?: string; + accessLevel?: string; + createdAt?: string; + modifiedAt?: string; +} + +export class SheetPathNode { + id: number; + name?: string; + permalink?: string; + accessLevel?: string; + folders?: SheetPathNode[]; + sheets?: SheetPathLeaf[]; + + constructor(data: Record) { + this.id = data.id as number; + this.name = data.name as string | undefined; + this.permalink = data.permalink as string | undefined; + this.accessLevel = data.accessLevel as string | undefined; + if (Array.isArray(data.folders)) { + this.folders = (data.folders as Record[]).map((f) => new SheetPathNode(f)); + } + if (Array.isArray(data.sheets)) { + this.sheets = data.sheets as SheetPathLeaf[]; + } + } + + private _walkToLeaf(): SheetPathNode[] { + if (this.sheets && this.sheets.length > 0) return [this]; + if (this.folders && this.folders.length > 0) { + return [this, ...this.folders[0]._walkToLeaf()]; + } + return [this]; + } + + /** Returns the target SheetPathLeaf sheet, or undefined if not reachable. */ + getSheet(): SheetPathLeaf | undefined { + for (const node of this._walkToLeaf()) { + if (node.sheets && node.sheets.length > 0) return node.sheets[0]; + } + return undefined; + } + + /** Returns a slash-separated path string from this node to the target sheet. */ + getSheetPath(): string | undefined { + const nodes = this._walkToLeaf(); + if (nodes.length === 0) return undefined; + const parts = nodes.map((n) => n.name).filter(Boolean) as string[]; + const leaf = nodes[nodes.length - 1]; + if (leaf.sheets && leaf.sheets.length > 0 && leaf.sheets[0].name) { + parts[parts.length - 1] = leaf.sheets[0].name; + } + return parts.length > 0 ? parts.join('/') : undefined; + } +} + export interface SheetUserSettings { /** * Does this user have "Show Critical Path" turned on for this sheet? NOTE: This setting only has an effect on project sheets with dependencies enabled. diff --git a/lib/sights/index.ts b/lib/sights/index.ts index 46bcf62f..e9ba8340 100644 --- a/lib/sights/index.ts +++ b/lib/sights/index.ts @@ -1,3 +1,4 @@ +import type { ApiError } from '../types/ApiError'; import type { CreateOptions } from '../types/CreateOptions'; import type { RequestCallback } from '../types/RequestCallback'; import type { RequestOptions } from '../types/RequestOptions'; @@ -19,7 +20,9 @@ import type { SightPublishStatus, SetSightPublishStatusOptions, SetSightPublishStatusResponse, + GetSightPathOptions, } from './types'; +import { SightPathNode } from './types'; export function create(options: CreateOptions): SightsApi { const requestor = options.requestor; @@ -77,6 +80,24 @@ export function create(options: CreateOptions): SightsApi { return requestor.put({ ...optionsToSend, ...urlOptions, ...putOptions }, callback); }; + const getSightPath = ( + getOptions: GetSightPathOptions, + callback?: RequestCallback + ): Promise => { + const urlOptions = { url: buildUrl(getOptions.sightId) + '/path' }; + return requestor + .get({ ...optionsToSend, ...urlOptions, ...getOptions }) + .then((data: Record) => { + const node = new SightPathNode(data); + if (callback) callback(undefined, node); + return node; + }) + .catch((err: unknown) => { + if (callback) callback(err as ApiError, undefined); + return Promise.reject(err); + }); + }; + const buildUrl = (sightId?: number | string) => { if (sightId !== undefined) { return options.apiUrls.sights + '/' + sightId; @@ -93,6 +114,7 @@ export function create(options: CreateOptions): SightsApi { moveSight, getSightPublishStatus, setSightPublishStatus, + getSightPath, }; } diff --git a/lib/sights/types.ts b/lib/sights/types.ts index e153206f..7c698c82 100644 --- a/lib/sights/types.ts +++ b/lib/sights/types.ts @@ -193,6 +193,28 @@ export interface SightsApi { options: SetSightPublishStatusOptions, callback?: RequestCallback ) => Promise; + + /** + * Gets the path from the workspace root to the specified sight (dashboard). + * + * @param options - {@link GetSightPathOptions} - Configuration options for the request + * @param callback - {@link RequestCallback}\<{@link SightPathNode}\> - Optional callback function + * @returns Promise\<{@link SightPathNode}\> + * + * @remarks + * It mirrors to the following Smartsheet REST API method: `GET /sights/{sightId}/path` + * + * @example + * ```typescript + * const path = await client.sights.getSightPath({ + * sightId: 123456789 + * }); + * ``` + */ + getSightPath: ( + options: GetSightPathOptions, + callback?: RequestCallback + ) => Promise; } // ============================================================================ @@ -585,3 +607,73 @@ export interface SetSightPublishStatusResponse extends BaseResponseStatus { */ result: SightPublishStatus; } + +// ============================================================================ +// Get Sight Path +// ============================================================================ + +export interface SightPathLeaf { + id: number; + name?: string; + permalink?: string; + accessLevel?: string; + createdAt?: string; + modifiedAt?: string; +} + +export class SightPathNode { + id: number; + name?: string; + permalink?: string; + accessLevel?: string; + folders?: SightPathNode[]; + sights?: SightPathLeaf[]; + + constructor(data: Record) { + this.id = data.id as number; + this.name = data.name as string | undefined; + this.permalink = data.permalink as string | undefined; + this.accessLevel = data.accessLevel as string | undefined; + if (Array.isArray(data.folders)) { + this.folders = (data.folders as Record[]).map((f) => new SightPathNode(f)); + } + if (Array.isArray(data.sights)) { + this.sights = data.sights as SightPathLeaf[]; + } + } + + private walkToLeaf(): SightPathNode[] { + if (this.sights && this.sights.length > 0) return [this]; + if (this.folders && this.folders.length > 0) { + return [this, ...this.folders[0].walkToLeaf()]; + } + return [this]; + } + + /** Returns the target SightPathLeaf sight, or undefined if not reachable. */ + getSight(): SightPathLeaf | undefined { + for (const node of this.walkToLeaf()) { + if (node.sights && node.sights.length > 0) return node.sights[0]; + } + return undefined; + } + + /** Returns a slash-separated path string from this node to the target sight. */ + getSightPath(): string | undefined { + const nodes = this.walkToLeaf(); + if (nodes.length === 0) return undefined; + const parts = nodes.map((n) => n.name).filter(Boolean) as string[]; + const leaf = nodes[nodes.length - 1]; + if (leaf.sights && leaf.sights.length > 0 && leaf.sights[0].name) { + parts[parts.length - 1] = leaf.sights[0].name; + } + return parts.length > 0 ? parts.join('/') : undefined; + } +} + +export interface GetSightPathOptions extends RequestOptions { + /** + * Sight Id. + */ + sightId: number; +} diff --git a/test/mock-api/folders/common_test_constants.ts b/test/mock-api/folders/common_test_constants.ts index 137af8f6..21e71443 100644 --- a/test/mock-api/folders/common_test_constants.ts +++ b/test/mock-api/folders/common_test_constants.ts @@ -1,3 +1,9 @@ +// Common Workspace Properties (used for path tests) +export const TEST_PATH_WORKSPACE_ID = 4509918431602564; +export const TEST_PATH_WORKSPACE_NAME = 'Sample Workspace'; +export const TEST_PATH_WORKSPACE_PERMALINK = + 'https://api.smartsheet.com/workspaces/cpG82pf5v8FPrgFfJrFcrM56xCHVGhmV4P4xcQ71'; + // Common Folder IDs export const TEST_FOLDER_ID = 7116448184199044; export const TEST_CHILD_FOLDER_ID_1 = 1234567890123456; diff --git a/test/mock-api/folders/get_folder_path.spec.ts b/test/mock-api/folders/get_folder_path.spec.ts new file mode 100644 index 00000000..60ef956e --- /dev/null +++ b/test/mock-api/folders/get_folder_path.spec.ts @@ -0,0 +1,165 @@ +import crypto from 'crypto'; +import { createClient, findWireMockRequest } from '../utils/utils'; +import { expect } from '@jest/globals'; +import { + TEST_FOLDER_ID, + TEST_PATH_WORKSPACE_ID, + TEST_PATH_WORKSPACE_NAME, + TEST_PATH_WORKSPACE_PERMALINK, + ERROR_500_STATUS_CODE, + ERROR_500_MESSAGE, + ERROR_400_STATUS_CODE, + ERROR_400_MESSAGE, +} from './common_test_constants'; +import { APIAccessLevel } from '@smartsheet/types'; +import { FolderPathNode } from '@smartsheet/folders/types'; + +describe('Folders - getFolderPath endpoint tests', () => { + const client = createClient(); + + it('getFolderPath generated url is correct', async () => { + const requestId = crypto.randomUUID(); + const options = { + folderId: TEST_FOLDER_ID, + customProperties: { + 'x-request-id': requestId, + 'x-test-name': '/folders/get-nested-folder-path/all-response-body-properties', + }, + }; + await client.folders.getFolderPath(options); + const matchedRequest = await findWireMockRequest(requestId); + const parsedUrl = new URL(matchedRequest.absoluteUrl); + expect(parsedUrl.pathname).toEqual(`/2.0/folders/${TEST_FOLDER_ID}/path`); + expect(matchedRequest.method).toEqual('GET'); + const queryParamsObject = Object.fromEntries(parsedUrl.searchParams); + expect(queryParamsObject).toEqual({}); + }); + + it('getFolderPath all response body properties', async () => { + const requestId = crypto.randomUUID(); + const options = { + folderId: TEST_FOLDER_ID, + customProperties: { + 'x-request-id': requestId, + 'x-test-name': '/folders/get-nested-folder-path/all-response-body-properties', + }, + }; + const response = await client.folders.getFolderPath(options); + const matchedRequest = await findWireMockRequest(requestId); + + expect(matchedRequest.body).toEqual(''); + expect(response).toEqual({ + id: TEST_PATH_WORKSPACE_ID, + name: TEST_PATH_WORKSPACE_NAME, + permalink: TEST_PATH_WORKSPACE_PERMALINK, + accessLevel: APIAccessLevel.owner, + folders: [ + { + id: 1234567890123456, + name: 'Project Plans', + permalink: 'https://app.smartsheet.com/folders/1234567890123456', + folders: [ + { + id: 2345678901234567, + name: 'Project Plans Subfolder', + permalink: 'https://app.smartsheet.com/folders/2345678901234567', + folders: [ + { + id: 3456789012345678, + name: 'Project Plans Sub-Subfolder', + permalink: 'https://app.smartsheet.com/folders/3456789012345678', + }, + ], + }, + ], + }, + ], + }); + }); + + it('getFolderPath root level response body properties', async () => { + const requestId = crypto.randomUUID(); + const options = { + folderId: TEST_FOLDER_ID, + customProperties: { + 'x-request-id': requestId, + 'x-test-name': '/folders/get-root-folder-path/all-response-body-properties', + }, + }; + const response = await client.folders.getFolderPath(options); + const matchedRequest = await findWireMockRequest(requestId); + + expect(matchedRequest.body).toEqual(''); + expect(response).toEqual({ + id: TEST_PATH_WORKSPACE_ID, + name: TEST_PATH_WORKSPACE_NAME, + permalink: TEST_PATH_WORKSPACE_PERMALINK, + accessLevel: APIAccessLevel.owner, + folders: [ + { + id: 5678901234567890, + name: 'Root Level Folder', + permalink: 'https://app.smartsheet.com/folders/rootlevel', + }, + ], + }); + }); + + it('getFolderPath returns FolderPathNode instance with getFolder and getFolderPath methods', async () => { + const requestId = crypto.randomUUID(); + const options = { + folderId: TEST_FOLDER_ID, + customProperties: { + 'x-request-id': requestId, + 'x-test-name': '/folders/get-nested-folder-path/all-response-body-properties', + }, + }; + const response = await client.folders.getFolderPath(options); + + expect(response).toBeInstanceOf(FolderPathNode); + expect(response.getFolder()).toEqual({ + id: 3456789012345678, + name: 'Project Plans Sub-Subfolder', + permalink: 'https://app.smartsheet.com/folders/3456789012345678', + }); + expect(response.getFolderPath()).toEqual( + 'Sample Workspace/Project Plans/Project Plans Subfolder/Project Plans Sub-Subfolder' + ); + }); + + it('getFolderPath error 500 response', async () => { + const requestId = crypto.randomUUID(); + const options = { + folderId: TEST_FOLDER_ID, + customProperties: { + 'x-request-id': requestId, + 'x-test-name': '/errors/500-response', + }, + }; + try { + await client.folders.getFolderPath(options); + expect(true).toBe(false); + } catch (error: any) { + expect(error.statusCode).toBe(ERROR_500_STATUS_CODE); + expect(error.message).toBe(ERROR_500_MESSAGE); + } + }); + + it('getFolderPath error 400 response', async () => { + const requestId = crypto.randomUUID(); + const options = { + folderId: TEST_FOLDER_ID, + customProperties: { + 'x-request-id': requestId, + 'x-test-name': '/errors/400-response', + }, + }; + try { + await client.folders.getFolderPath(options); + expect(true).toBe(false); + } catch (error: any) { + expect(error.statusCode).toBe(ERROR_400_STATUS_CODE); + expect(error.message).toBe(ERROR_400_MESSAGE); + } + }); +}); diff --git a/test/mock-api/reports/common_test_constants.ts b/test/mock-api/reports/common_test_constants.ts index b33eb68a..3aada646 100644 --- a/test/mock-api/reports/common_test_constants.ts +++ b/test/mock-api/reports/common_test_constants.ts @@ -1,5 +1,15 @@ import { ReportColumnType } from '@smartsheet/reports/types'; +// Common Workspace Properties (used for path tests) +export const TEST_PATH_WORKSPACE_ID = 4509918431602564; +export const TEST_PATH_WORKSPACE_NAME = 'Sample Workspace'; +export const TEST_PATH_WORKSPACE_PERMALINK = + 'https://api.smartsheet.com/workspaces/cpG82pf5v8FPrgFfJrFcrM56xCHVGhmV4P4xcQ71'; + +// Common Path Timestamps +export const TEST_REPORT_CREATED_AT = '2024-01-01T00:00:00Z'; +export const TEST_REPORT_MODIFIED_AT = '2024-06-01T00:00:00Z'; + // Common Report IDs export const TEST_REPORT_ID = 4583173393803140; diff --git a/test/mock-api/reports/get_report_path.spec.ts b/test/mock-api/reports/get_report_path.spec.ts new file mode 100644 index 00000000..d7fe85c0 --- /dev/null +++ b/test/mock-api/reports/get_report_path.spec.ts @@ -0,0 +1,174 @@ +import crypto from 'crypto'; +import { createClient, findWireMockRequest } from '../utils/utils'; +import { expect } from '@jest/globals'; +import { APIAccessLevel } from '@smartsheet/types'; +import { + TEST_REPORT_ID, + TEST_REPORT_CREATED_AT, + TEST_REPORT_MODIFIED_AT, + TEST_PATH_WORKSPACE_ID, + TEST_PATH_WORKSPACE_NAME, + TEST_PATH_WORKSPACE_PERMALINK, + ERROR_500_STATUS_CODE, + ERROR_500_MESSAGE, + ERROR_400_STATUS_CODE, + ERROR_400_MESSAGE, +} from './common_test_constants'; +import { ReportPathNode } from '@smartsheet/reports/types'; + +describe('Reports - getReportPath endpoint tests', () => { + const client = createClient(); + + it('getReportPath generated url is correct', async () => { + const requestId = crypto.randomUUID(); + const options = { + reportId: TEST_REPORT_ID, + customProperties: { + 'x-request-id': requestId, + 'x-test-name': '/reports/get-nested-report-path/all-response-body-properties', + }, + }; + await client.reports.getReportPath(options); + const matchedRequest = await findWireMockRequest(requestId); + const parsedUrl = new URL(matchedRequest.absoluteUrl); + expect(parsedUrl.pathname).toEqual(`/2.0/reports/${TEST_REPORT_ID}/path`); + expect(matchedRequest.method).toEqual('GET'); + const queryParamsObject = Object.fromEntries(parsedUrl.searchParams); + expect(queryParamsObject).toEqual({}); + }); + + it('getReportPath all response body properties', async () => { + const requestId = crypto.randomUUID(); + const options = { + reportId: TEST_REPORT_ID, + customProperties: { + 'x-request-id': requestId, + 'x-test-name': '/reports/get-nested-report-path/all-response-body-properties', + }, + }; + const response = await client.reports.getReportPath(options); + const matchedRequest = await findWireMockRequest(requestId); + + expect(matchedRequest.body).toEqual(''); + expect(response).toEqual({ + id: TEST_PATH_WORKSPACE_ID, + name: TEST_PATH_WORKSPACE_NAME, + permalink: TEST_PATH_WORKSPACE_PERMALINK, + accessLevel: APIAccessLevel.owner, + folders: [ + { + id: 1234567890123456, + name: 'Project Plans', + permalink: 'https://app.smartsheet.com/folders/1234567890123456', + folders: [ + { + id: 2345678901234567, + name: 'Project Plans Subfolder', + permalink: 'https://app.smartsheet.com/folders/2345678901234567', + reports: [ + { + id: 3456789012345678, + name: 'Project Report', + permalink: 'https://app.smartsheet.com/reports/3456789012345678', + accessLevel: APIAccessLevel.admin, + createdAt: TEST_REPORT_CREATED_AT, + modifiedAt: TEST_REPORT_MODIFIED_AT, + }, + ], + }, + ], + }, + ], + }); + }); + + it('getReportPath root level response body properties', async () => { + const requestId = crypto.randomUUID(); + const options = { + reportId: TEST_REPORT_ID, + customProperties: { + 'x-request-id': requestId, + 'x-test-name': '/reports/get-root-report-path/all-response-body-properties', + }, + }; + const response = await client.reports.getReportPath(options); + const matchedRequest = await findWireMockRequest(requestId); + + expect(matchedRequest.body).toEqual(''); + expect(response).toEqual({ + id: TEST_PATH_WORKSPACE_ID, + name: TEST_PATH_WORKSPACE_NAME, + permalink: TEST_PATH_WORKSPACE_PERMALINK, + accessLevel: APIAccessLevel.owner, + reports: [ + { + id: 5678901234567890, + name: 'Root Level Report', + permalink: 'https://app.smartsheet.com/reports/rootlevel', + accessLevel: APIAccessLevel.admin, + createdAt: TEST_REPORT_CREATED_AT, + modifiedAt: TEST_REPORT_MODIFIED_AT, + }, + ], + }); + }); + + it('getReportPath returns ReportPathNode instance with getReport and getReportPath methods', async () => { + const requestId = crypto.randomUUID(); + const options = { + reportId: TEST_REPORT_ID, + customProperties: { + 'x-request-id': requestId, + 'x-test-name': '/reports/get-nested-report-path/all-response-body-properties', + }, + }; + const response = await client.reports.getReportPath(options); + + expect(response).toBeInstanceOf(ReportPathNode); + expect(response.getReport()).toEqual({ + id: 3456789012345678, + name: 'Project Report', + permalink: 'https://app.smartsheet.com/reports/3456789012345678', + accessLevel: APIAccessLevel.admin, + createdAt: TEST_REPORT_CREATED_AT, + modifiedAt: TEST_REPORT_MODIFIED_AT, + }); + expect(response.getReportPath()).toEqual('Sample Workspace/Project Plans/Project Report'); + }); + + it('getReportPath error 500 response', async () => { + const requestId = crypto.randomUUID(); + const options = { + reportId: TEST_REPORT_ID, + customProperties: { + 'x-request-id': requestId, + 'x-test-name': '/errors/500-response', + }, + }; + try { + await client.reports.getReportPath(options); + expect(true).toBe(false); + } catch (error: any) { + expect(error.statusCode).toBe(ERROR_500_STATUS_CODE); + expect(error.message).toBe(ERROR_500_MESSAGE); + } + }); + + it('getReportPath error 400 response', async () => { + const requestId = crypto.randomUUID(); + const options = { + reportId: TEST_REPORT_ID, + customProperties: { + 'x-request-id': requestId, + 'x-test-name': '/errors/400-response', + }, + }; + try { + await client.reports.getReportPath(options); + expect(true).toBe(false); + } catch (error: any) { + expect(error.statusCode).toBe(ERROR_400_STATUS_CODE); + expect(error.message).toBe(ERROR_400_MESSAGE); + } + }); +}); diff --git a/test/mock-api/sheets/common_test_constants.ts b/test/mock-api/sheets/common_test_constants.ts new file mode 100644 index 00000000..aa15274c --- /dev/null +++ b/test/mock-api/sheets/common_test_constants.ts @@ -0,0 +1,16 @@ +// Common Workspace Properties (used for path tests) +export const TEST_PATH_WORKSPACE_ID = 4509918431602564; +export const TEST_PATH_WORKSPACE_NAME = 'Sample Workspace'; +export const TEST_PATH_WORKSPACE_PERMALINK = + 'https://api.smartsheet.com/workspaces/cpG82pf5v8FPrgFfJrFcrM56xCHVGhmV4P4xcQ71'; + +// Common Sheet Data +export const TEST_SHEET_ID = 9876543210987654; +export const TEST_SHEET_CREATED_AT = '2024-01-01T00:00:00Z'; +export const TEST_SHEET_MODIFIED_AT = '2024-06-01T00:00:00Z'; + +// Common Error Status Codes +export const ERROR_500_STATUS_CODE = 500; +export const ERROR_500_MESSAGE = 'Internal Server Error'; +export const ERROR_400_STATUS_CODE = 400; +export const ERROR_400_MESSAGE = 'Malformed Request'; diff --git a/test/mock-api/sheets/get_sheet_path.spec.ts b/test/mock-api/sheets/get_sheet_path.spec.ts new file mode 100644 index 00000000..45da791c --- /dev/null +++ b/test/mock-api/sheets/get_sheet_path.spec.ts @@ -0,0 +1,174 @@ +import crypto from 'crypto'; +import { createClient, findWireMockRequest } from '../utils/utils'; +import { expect } from '@jest/globals'; +import { APIAccessLevel } from '@smartsheet/types'; +import { SheetPathNode } from '@smartsheet/sheets/types'; +import { + TEST_SHEET_ID, + TEST_SHEET_CREATED_AT, + TEST_SHEET_MODIFIED_AT, + TEST_PATH_WORKSPACE_ID, + TEST_PATH_WORKSPACE_NAME, + TEST_PATH_WORKSPACE_PERMALINK, + ERROR_500_STATUS_CODE, + ERROR_500_MESSAGE, + ERROR_400_STATUS_CODE, + ERROR_400_MESSAGE, +} from './common_test_constants'; + +describe('Sheets - getSheetPath endpoint tests', () => { + const client = createClient(); + + it('getSheetPath generated url is correct', async () => { + const requestId = crypto.randomUUID(); + const options = { + sheetId: TEST_SHEET_ID, + customProperties: { + 'x-request-id': requestId, + 'x-test-name': '/sheets/get-nested-sheet-path/all-response-body-properties', + }, + }; + await client.sheets.getSheetPath(options); + const matchedRequest = await findWireMockRequest(requestId); + const parsedUrl = new URL(matchedRequest.absoluteUrl); + expect(parsedUrl.pathname).toEqual(`/2.0/sheets/${TEST_SHEET_ID}/path`); + expect(matchedRequest.method).toEqual('GET'); + const queryParamsObject = Object.fromEntries(parsedUrl.searchParams); + expect(queryParamsObject).toEqual({}); + }); + + it('getSheetPath all response body properties', async () => { + const requestId = crypto.randomUUID(); + const options = { + sheetId: TEST_SHEET_ID, + customProperties: { + 'x-request-id': requestId, + 'x-test-name': '/sheets/get-nested-sheet-path/all-response-body-properties', + }, + }; + const response = await client.sheets.getSheetPath(options); + const matchedRequest = await findWireMockRequest(requestId); + + expect(matchedRequest.body).toEqual(''); + expect(response).toEqual({ + id: TEST_PATH_WORKSPACE_ID, + name: TEST_PATH_WORKSPACE_NAME, + permalink: TEST_PATH_WORKSPACE_PERMALINK, + accessLevel: APIAccessLevel.owner, + folders: [ + { + id: 1234567890123456, + name: 'Project Plans', + permalink: 'https://app.smartsheet.com/folders/1234567890123456', + folders: [ + { + id: 2345678901234567, + name: 'Project Plans Subfolder', + permalink: 'https://app.smartsheet.com/folders/2345678901234567', + sheets: [ + { + id: 3456789012345678, + name: 'Project Plan', + permalink: 'https://app.smartsheet.com/sheets/3456789012345678', + accessLevel: APIAccessLevel.admin, + createdAt: TEST_SHEET_CREATED_AT, + modifiedAt: TEST_SHEET_MODIFIED_AT, + }, + ], + }, + ], + }, + ], + }); + }); + + it('getSheetPath root level response body properties', async () => { + const requestId = crypto.randomUUID(); + const options = { + sheetId: TEST_SHEET_ID, + customProperties: { + 'x-request-id': requestId, + 'x-test-name': '/sheets/get-root-sheet-path/all-response-body-properties', + }, + }; + const response = await client.sheets.getSheetPath(options); + const matchedRequest = await findWireMockRequest(requestId); + + expect(matchedRequest.body).toEqual(''); + expect(response).toEqual({ + id: TEST_PATH_WORKSPACE_ID, + name: TEST_PATH_WORKSPACE_NAME, + permalink: TEST_PATH_WORKSPACE_PERMALINK, + accessLevel: APIAccessLevel.owner, + sheets: [ + { + id: 5678901234567890, + name: 'Root Level Sheet', + permalink: 'https://app.smartsheet.com/sheets/rootlevel', + accessLevel: APIAccessLevel.admin, + createdAt: TEST_SHEET_CREATED_AT, + modifiedAt: TEST_SHEET_MODIFIED_AT, + }, + ], + }); + }); + + it('getSheetPath returns SheetPathNode instance with getSheet and getSheetPath methods', async () => { + const requestId = crypto.randomUUID(); + const options = { + sheetId: TEST_SHEET_ID, + customProperties: { + 'x-request-id': requestId, + 'x-test-name': '/sheets/get-nested-sheet-path/all-response-body-properties', + }, + }; + const response = await client.sheets.getSheetPath(options); + + expect(response).toBeInstanceOf(SheetPathNode); + expect(response.getSheet()).toEqual({ + id: 3456789012345678, + name: 'Project Plan', + permalink: 'https://app.smartsheet.com/sheets/3456789012345678', + accessLevel: APIAccessLevel.admin, + createdAt: TEST_SHEET_CREATED_AT, + modifiedAt: TEST_SHEET_MODIFIED_AT, + }); + expect(response.getSheetPath()).toEqual('Sample Workspace/Project Plans/Project Plan'); + }); + + it('getSheetPath error 500 response', async () => { + const requestId = crypto.randomUUID(); + const options = { + sheetId: TEST_SHEET_ID, + customProperties: { + 'x-request-id': requestId, + 'x-test-name': '/errors/500-response', + }, + }; + try { + await client.sheets.getSheetPath(options); + expect(true).toBe(false); + } catch (error: any) { + expect(error.statusCode).toBe(ERROR_500_STATUS_CODE); + expect(error.message).toBe(ERROR_500_MESSAGE); + } + }); + + it('getSheetPath error 400 response', async () => { + const requestId = crypto.randomUUID(); + const options = { + sheetId: TEST_SHEET_ID, + customProperties: { + 'x-request-id': requestId, + 'x-test-name': '/errors/400-response', + }, + }; + try { + await client.sheets.getSheetPath(options); + expect(true).toBe(false); + } catch (error: any) { + expect(error.statusCode).toBe(ERROR_400_STATUS_CODE); + expect(error.message).toBe(ERROR_400_MESSAGE); + } + }); +}); diff --git a/test/mock-api/sights/common_test_constants.ts b/test/mock-api/sights/common_test_constants.ts new file mode 100644 index 00000000..1a8c4cb1 --- /dev/null +++ b/test/mock-api/sights/common_test_constants.ts @@ -0,0 +1,16 @@ +// Common Workspace Properties (used for path tests) +export const TEST_PATH_WORKSPACE_ID = 4509918431602564; +export const TEST_PATH_WORKSPACE_NAME = 'Sample Workspace'; +export const TEST_PATH_WORKSPACE_PERMALINK = + 'https://api.smartsheet.com/workspaces/cpG82pf5v8FPrgFfJrFcrM56xCHVGhmV4P4xcQ71'; + +// Common Sight IDs +export const TEST_SIGHT_ID = 9876543210123456; +export const TEST_SIGHT_CREATED_AT = '2024-01-01T00:00:00Z'; +export const TEST_SIGHT_MODIFIED_AT = '2024-06-01T00:00:00Z'; + +// Common Error Status Codes +export const ERROR_500_STATUS_CODE = 500; +export const ERROR_500_MESSAGE = 'Internal Server Error'; +export const ERROR_400_STATUS_CODE = 400; +export const ERROR_400_MESSAGE = 'Malformed Request'; diff --git a/test/mock-api/sights/get_sight_path.spec.ts b/test/mock-api/sights/get_sight_path.spec.ts new file mode 100644 index 00000000..e2962e1e --- /dev/null +++ b/test/mock-api/sights/get_sight_path.spec.ts @@ -0,0 +1,174 @@ +import crypto from 'crypto'; +import { createClient, findWireMockRequest } from '../utils/utils'; +import { expect } from '@jest/globals'; +import { APIAccessLevel } from '@smartsheet/types'; +import { SightPathNode } from '@smartsheet/sights/types'; +import { + TEST_SIGHT_ID, + TEST_SIGHT_CREATED_AT, + TEST_SIGHT_MODIFIED_AT, + TEST_PATH_WORKSPACE_ID, + TEST_PATH_WORKSPACE_NAME, + TEST_PATH_WORKSPACE_PERMALINK, + ERROR_500_STATUS_CODE, + ERROR_500_MESSAGE, + ERROR_400_STATUS_CODE, + ERROR_400_MESSAGE, +} from './common_test_constants'; + +describe('Sights - getSightPath endpoint tests', () => { + const client = createClient(); + + it('getSightPath generated url is correct', async () => { + const requestId = crypto.randomUUID(); + const options = { + sightId: TEST_SIGHT_ID, + customProperties: { + 'x-request-id': requestId, + 'x-test-name': '/sights/get-nested-sight-path/all-response-body-properties', + }, + }; + await client.sights.getSightPath(options); + const matchedRequest = await findWireMockRequest(requestId); + const parsedUrl = new URL(matchedRequest.absoluteUrl); + expect(parsedUrl.pathname).toEqual(`/2.0/sights/${TEST_SIGHT_ID}/path`); + expect(matchedRequest.method).toEqual('GET'); + const queryParamsObject = Object.fromEntries(parsedUrl.searchParams); + expect(queryParamsObject).toEqual({}); + }); + + it('getSightPath all response body properties', async () => { + const requestId = crypto.randomUUID(); + const options = { + sightId: TEST_SIGHT_ID, + customProperties: { + 'x-request-id': requestId, + 'x-test-name': '/sights/get-nested-sight-path/all-response-body-properties', + }, + }; + const response = await client.sights.getSightPath(options); + const matchedRequest = await findWireMockRequest(requestId); + + expect(matchedRequest.body).toEqual(''); + expect(response).toEqual({ + id: TEST_PATH_WORKSPACE_ID, + name: TEST_PATH_WORKSPACE_NAME, + permalink: TEST_PATH_WORKSPACE_PERMALINK, + accessLevel: APIAccessLevel.owner, + folders: [ + { + id: 1234567890123456, + name: 'Project Plans', + permalink: 'https://app.smartsheet.com/folders/1234567890123456', + folders: [ + { + id: 2345678901234567, + name: 'Project Plans Subfolder', + permalink: 'https://app.smartsheet.com/folders/2345678901234567', + sights: [ + { + id: 3456789012345678, + name: 'Project Dashboard', + permalink: 'https://app.smartsheet.com/dashboards/3456789012345678', + accessLevel: APIAccessLevel.admin, + createdAt: TEST_SIGHT_CREATED_AT, + modifiedAt: TEST_SIGHT_MODIFIED_AT, + }, + ], + }, + ], + }, + ], + }); + }); + + it('getSightPath root level response body properties', async () => { + const requestId = crypto.randomUUID(); + const options = { + sightId: TEST_SIGHT_ID, + customProperties: { + 'x-request-id': requestId, + 'x-test-name': '/sights/get-root-sight-path/all-response-body-properties', + }, + }; + const response = await client.sights.getSightPath(options); + const matchedRequest = await findWireMockRequest(requestId); + + expect(matchedRequest.body).toEqual(''); + expect(response).toEqual({ + id: TEST_PATH_WORKSPACE_ID, + name: TEST_PATH_WORKSPACE_NAME, + permalink: TEST_PATH_WORKSPACE_PERMALINK, + accessLevel: APIAccessLevel.owner, + sights: [ + { + id: 5678901234567890, + name: 'Root Level Dashboard', + permalink: 'https://app.smartsheet.com/dashboards/rootlevel', + accessLevel: APIAccessLevel.admin, + createdAt: TEST_SIGHT_CREATED_AT, + modifiedAt: TEST_SIGHT_MODIFIED_AT, + }, + ], + }); + }); + + it('getSightPath returns SightPathNode instance with getSight and getSightPath methods', async () => { + const requestId = crypto.randomUUID(); + const options = { + sightId: TEST_SIGHT_ID, + customProperties: { + 'x-request-id': requestId, + 'x-test-name': '/sights/get-nested-sight-path/all-response-body-properties', + }, + }; + const response = await client.sights.getSightPath(options); + + expect(response).toBeInstanceOf(SightPathNode); + expect(response.getSight()).toEqual({ + id: 3456789012345678, + name: 'Project Dashboard', + permalink: 'https://app.smartsheet.com/dashboards/3456789012345678', + accessLevel: APIAccessLevel.admin, + createdAt: TEST_SIGHT_CREATED_AT, + modifiedAt: TEST_SIGHT_MODIFIED_AT, + }); + expect(response.getSightPath()).toEqual('Sample Workspace/Project Plans/Project Dashboard'); + }); + + it('getSightPath error 500 response', async () => { + const requestId = crypto.randomUUID(); + const options = { + sightId: TEST_SIGHT_ID, + customProperties: { + 'x-request-id': requestId, + 'x-test-name': '/errors/500-response', + }, + }; + try { + await client.sights.getSightPath(options); + expect(true).toBe(false); + } catch (error: any) { + expect(error.statusCode).toBe(ERROR_500_STATUS_CODE); + expect(error.message).toBe(ERROR_500_MESSAGE); + } + }); + + it('getSightPath error 400 response', async () => { + const requestId = crypto.randomUUID(); + const options = { + sightId: TEST_SIGHT_ID, + customProperties: { + 'x-request-id': requestId, + 'x-test-name': '/errors/400-response', + }, + }; + try { + await client.sights.getSightPath(options); + expect(true).toBe(false); + } catch (error: any) { + expect(error.statusCode).toBe(ERROR_400_STATUS_CODE); + expect(error.message).toBe(ERROR_400_MESSAGE); + } + }); +}); From 428b17c40cfc7da2440ae39c78ff79093a1f4ae2 Mon Sep 17 00:00:00 2001 From: Andrey Strinski Date: Thu, 11 Jun 2026 10:11:44 +0300 Subject: [PATCH 02/11] Update workspace constant for tests --- test/mock-api/folders/common_test_constants.ts | 2 +- test/mock-api/reports/common_test_constants.ts | 2 +- test/mock-api/sheets/common_test_constants.ts | 2 +- test/mock-api/sights/common_test_constants.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/mock-api/folders/common_test_constants.ts b/test/mock-api/folders/common_test_constants.ts index 21e71443..298310e1 100644 --- a/test/mock-api/folders/common_test_constants.ts +++ b/test/mock-api/folders/common_test_constants.ts @@ -2,7 +2,7 @@ export const TEST_PATH_WORKSPACE_ID = 4509918431602564; export const TEST_PATH_WORKSPACE_NAME = 'Sample Workspace'; export const TEST_PATH_WORKSPACE_PERMALINK = - 'https://api.smartsheet.com/workspaces/cpG82pf5v8FPrgFfJrFcrM56xCHVGhmV4P4xcQ71'; + 'https://app.smartsheet.com/workspaces/mock_workspace_id'; // Common Folder IDs export const TEST_FOLDER_ID = 7116448184199044; diff --git a/test/mock-api/reports/common_test_constants.ts b/test/mock-api/reports/common_test_constants.ts index 3aada646..79ff7483 100644 --- a/test/mock-api/reports/common_test_constants.ts +++ b/test/mock-api/reports/common_test_constants.ts @@ -4,7 +4,7 @@ import { ReportColumnType } from '@smartsheet/reports/types'; export const TEST_PATH_WORKSPACE_ID = 4509918431602564; export const TEST_PATH_WORKSPACE_NAME = 'Sample Workspace'; export const TEST_PATH_WORKSPACE_PERMALINK = - 'https://api.smartsheet.com/workspaces/cpG82pf5v8FPrgFfJrFcrM56xCHVGhmV4P4xcQ71'; + 'https://app.smartsheet.com/workspaces/mock_workspace_id'; // Common Path Timestamps export const TEST_REPORT_CREATED_AT = '2024-01-01T00:00:00Z'; diff --git a/test/mock-api/sheets/common_test_constants.ts b/test/mock-api/sheets/common_test_constants.ts index aa15274c..c7e99667 100644 --- a/test/mock-api/sheets/common_test_constants.ts +++ b/test/mock-api/sheets/common_test_constants.ts @@ -2,7 +2,7 @@ export const TEST_PATH_WORKSPACE_ID = 4509918431602564; export const TEST_PATH_WORKSPACE_NAME = 'Sample Workspace'; export const TEST_PATH_WORKSPACE_PERMALINK = - 'https://api.smartsheet.com/workspaces/cpG82pf5v8FPrgFfJrFcrM56xCHVGhmV4P4xcQ71'; + 'https://app.smartsheet.com/workspaces/mock_workspace_id'; // Common Sheet Data export const TEST_SHEET_ID = 9876543210987654; diff --git a/test/mock-api/sights/common_test_constants.ts b/test/mock-api/sights/common_test_constants.ts index 1a8c4cb1..9369e77c 100644 --- a/test/mock-api/sights/common_test_constants.ts +++ b/test/mock-api/sights/common_test_constants.ts @@ -2,7 +2,7 @@ export const TEST_PATH_WORKSPACE_ID = 4509918431602564; export const TEST_PATH_WORKSPACE_NAME = 'Sample Workspace'; export const TEST_PATH_WORKSPACE_PERMALINK = - 'https://api.smartsheet.com/workspaces/cpG82pf5v8FPrgFfJrFcrM56xCHVGhmV4P4xcQ71'; + 'https://app.smartsheet.com/workspaces/mock_workspace_id'; // Common Sight IDs export const TEST_SIGHT_ID = 9876543210123456; From 082a8d2889208627f8cfdae7799ccfd4dc74af77 Mon Sep 17 00:00:00 2001 From: Andrey Strinski Date: Thu, 11 Jun 2026 15:08:54 +0300 Subject: [PATCH 03/11] Fix style --- lib/folders/types.ts | 5 +---- lib/reports/types.ts | 5 +---- lib/sights/types.ts | 5 +---- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/lib/folders/types.ts b/lib/folders/types.ts index d2cbbf1e..df527a6a 100644 --- a/lib/folders/types.ts +++ b/lib/folders/types.ts @@ -214,10 +214,7 @@ export interface FoldersApi { * }); * ``` */ - getFolderPath: ( - options: GetFolderPathOptions, - callback?: RequestCallback - ) => Promise; + getFolderPath: (options: GetFolderPathOptions, callback?: RequestCallback) => Promise; } // ============================================================================ diff --git a/lib/reports/types.ts b/lib/reports/types.ts index 4e55dda1..22f7baaf 100644 --- a/lib/reports/types.ts +++ b/lib/reports/types.ts @@ -448,10 +448,7 @@ export interface ReportsApi { * }); * ``` */ - getReportPath: ( - options: GetReportPathOptions, - callback?: RequestCallback - ) => Promise; + getReportPath: (options: GetReportPathOptions, callback?: RequestCallback) => Promise; } // ============================================================================ diff --git a/lib/sights/types.ts b/lib/sights/types.ts index 7c698c82..5c13987f 100644 --- a/lib/sights/types.ts +++ b/lib/sights/types.ts @@ -211,10 +211,7 @@ export interface SightsApi { * }); * ``` */ - getSightPath: ( - options: GetSightPathOptions, - callback?: RequestCallback - ) => Promise; + getSightPath: (options: GetSightPathOptions, callback?: RequestCallback) => Promise; } // ============================================================================ From be39803daaef08b776a9fedf59c2f308e25d60c1 Mon Sep 17 00:00:00 2001 From: Andrey Strinski Date: Thu, 11 Jun 2026 15:21:33 +0300 Subject: [PATCH 04/11] Fix tests --- test/functional/client.spec.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/functional/client.spec.ts b/test/functional/client.spec.ts index 164b8d10..5210b990 100644 --- a/test/functional/client.spec.ts +++ b/test/functional/client.spec.ts @@ -87,12 +87,13 @@ describe('Client Unit Tests', () => { describe('#folders', () => { it('should have folders object', () => { expect(smartsheet).toHaveProperty('folders'); - expect(Object.keys(smartsheet.folders)).toHaveLength(7); + expect(Object.keys(smartsheet.folders)).toHaveLength(8); }); it('should have get methods', () => { expect(smartsheet.folders).toHaveProperty('getFolderMetadata'); expect(smartsheet.folders).toHaveProperty('getFolderChildren'); + expect(smartsheet.folders).toHaveProperty('getFolderPath'); }); it('should have create methods', () => { @@ -178,7 +179,7 @@ describe('Client Unit Tests', () => { describe('#reports', () => { it('should have reports object', () => { expect(smartsheet).toHaveProperty('reports'); - expect(Object.keys(smartsheet.reports)).toHaveLength(13); + expect(Object.keys(smartsheet.reports)).toHaveLength(14); }); it('should have get methods', () => { @@ -187,6 +188,7 @@ describe('Client Unit Tests', () => { expect(smartsheet.reports).toHaveProperty('getReportAsExcel'); expect(smartsheet.reports).toHaveProperty('getReportAsCSV'); expect(smartsheet.reports).toHaveProperty('getReportPublishStatus'); + expect(smartsheet.reports).toHaveProperty('getReportPath'); }); it('should have create methods', () => { @@ -314,13 +316,14 @@ describe('Client Unit Tests', () => { describe('#Sights', () => { it('should have Sights object', () => { expect(smartsheet).toHaveProperty('sights'); - expect(Object.keys(smartsheet.sights)).toHaveLength(8); + expect(Object.keys(smartsheet.sights)).toHaveLength(9); }); it('should have Sights get methods', () => { expect(smartsheet.sights).toHaveProperty('getSight'); expect(smartsheet.sights).toHaveProperty('listSights'); expect(smartsheet.sights).toHaveProperty('getSightPublishStatus'); + expect(smartsheet.sights).toHaveProperty('getSightPath'); }); it('should have Sights update methods', () => { From 134e1d119a2b8a0a8a86a1941f1471e6226cf063 Mon Sep 17 00:00:00 2001 From: Andrey Strinski Date: Thu, 11 Jun 2026 16:03:13 +0300 Subject: [PATCH 05/11] Clean up model --- lib/folders/types.ts | 22 +++++++--------------- lib/reports/types.ts | 23 ++++++++--------------- lib/sheets/types.ts | 28 +++++++++++----------------- lib/sights/index.ts | 14 +++++++------- lib/sights/types.ts | 30 +++++++++++------------------- lib/types/PathLeaf.ts | 8 ++++++++ lib/types/index.ts | 1 + 7 files changed, 53 insertions(+), 73 deletions(-) create mode 100644 lib/types/PathLeaf.ts diff --git a/lib/folders/types.ts b/lib/folders/types.ts index df527a6a..63931071 100644 --- a/lib/folders/types.ts +++ b/lib/folders/types.ts @@ -2,6 +2,7 @@ import type { RequestCallback } from '../types/RequestCallback'; import type { RequestOptions } from '../types/RequestOptions'; import type { BaseResponseStatus } from '../types/BaseResponseStatus'; import type { FailedItem } from '../types/FailedItem'; +import { APIAccessLevel } from '@smartsheet/types'; // ============================================================================ // Folders API Interface @@ -740,27 +741,18 @@ export interface MoveFolderResponse extends BaseResponseStatus { // Get Folder Path // ============================================================================ -export interface PathLeaf { - id: number; - name?: string; - permalink?: string; - accessLevel?: string; - createdAt?: string; - modifiedAt?: string; -} - export class FolderPathNode { id: number; - name?: string; - permalink?: string; - accessLevel?: string; + name: string; + permalink: string; + accessLevel?: APIAccessLevel; folders?: FolderPathNode[]; constructor(data: Record) { this.id = data.id as number; - this.name = data.name as string | undefined; - this.permalink = data.permalink as string | undefined; - this.accessLevel = data.accessLevel as string | undefined; + this.name = data.name as string; + this.permalink = data.permalink as string; + this.accessLevel = data.accessLevel as APIAccessLevel | undefined; if (Array.isArray(data.folders)) { this.folders = (data.folders as Record[]).map((f) => new FolderPathNode(f)); } diff --git a/lib/reports/types.ts b/lib/reports/types.ts index 22f7baaf..e36522d9 100644 --- a/lib/reports/types.ts +++ b/lib/reports/types.ts @@ -1,3 +1,4 @@ +import type { PathLeaf } from '../types/PathLeaf'; import type { RequestCallback } from '../types/RequestCallback'; import type { RequestOptions } from '../types/RequestOptions'; import type { BaseResponseStatus } from '../types/BaseResponseStatus'; @@ -10,6 +11,7 @@ import type { CrossSheetReference } from '../cross-sheet-references/types'; import type { SheetSummary } from '../sheet-summary/types'; import type { SheetUserSettings } from '../sheets/types'; import type { WorkspaceListing } from '../workspaces/types'; +import { APIAccessLevel } from '@smartsheet/types'; // ============================================================================ // Reports API Interface @@ -1663,28 +1665,19 @@ export interface CreateReportResponse extends BaseResponseStatus { // Get Report Path // ============================================================================ -export interface PathLeaf { - id: number; - name?: string; - permalink?: string; - accessLevel?: string; - createdAt?: string; - modifiedAt?: string; -} - export class ReportPathNode { id: number; - name?: string; - permalink?: string; - accessLevel?: string; + name: string; + permalink: string; + accessLevel?: APIAccessLevel; folders?: ReportPathNode[]; reports?: PathLeaf[]; constructor(data: Record) { this.id = data.id as number; - this.name = data.name as string | undefined; - this.permalink = data.permalink as string | undefined; - this.accessLevel = data.accessLevel as string | undefined; + this.name = data.name as string; + this.permalink = data.permalink as string; + this.accessLevel = data.accessLevel as APIAccessLevel | undefined; if (Array.isArray(data.folders)) { this.folders = (data.folders as Record[]).map((f) => new ReportPathNode(f)); } diff --git a/lib/sheets/types.ts b/lib/sheets/types.ts index be70b667..1f948a6d 100644 --- a/lib/sheets/types.ts +++ b/lib/sheets/types.ts @@ -2,33 +2,27 @@ // Get Sheet Path // ============================================================================ -export interface SheetPathLeaf { - id: number; - name?: string; - permalink?: string; - accessLevel?: string; - createdAt?: string; - modifiedAt?: string; -} +import type { PathLeaf } from '../types/PathLeaf'; +import type { APIAccessLevel } from '@smartsheet/types'; export class SheetPathNode { id: number; - name?: string; - permalink?: string; + name: string; + permalink: string; accessLevel?: string; folders?: SheetPathNode[]; - sheets?: SheetPathLeaf[]; + sheets?: PathLeaf[]; constructor(data: Record) { this.id = data.id as number; - this.name = data.name as string | undefined; - this.permalink = data.permalink as string | undefined; - this.accessLevel = data.accessLevel as string | undefined; + this.name = data.name as string; + this.permalink = data.permalink as string; + this.accessLevel = data.accessLevel as APIAccessLevel | undefined; if (Array.isArray(data.folders)) { this.folders = (data.folders as Record[]).map((f) => new SheetPathNode(f)); } if (Array.isArray(data.sheets)) { - this.sheets = data.sheets as SheetPathLeaf[]; + this.sheets = data.sheets as PathLeaf[]; } } @@ -40,8 +34,8 @@ export class SheetPathNode { return [this]; } - /** Returns the target SheetPathLeaf sheet, or undefined if not reachable. */ - getSheet(): SheetPathLeaf | undefined { + /** Returns the target PathLeaf sheet, or undefined if not reachable. */ + getSheet(): PathLeaf | undefined { for (const node of this._walkToLeaf()) { if (node.sheets && node.sheets.length > 0) return node.sheets[0]; } diff --git a/lib/sights/index.ts b/lib/sights/index.ts index e9ba8340..d71de54e 100644 --- a/lib/sights/index.ts +++ b/lib/sights/index.ts @@ -80,6 +80,13 @@ export function create(options: CreateOptions): SightsApi { return requestor.put({ ...optionsToSend, ...urlOptions, ...putOptions }, callback); }; + const buildUrl = (sightId?: number | string) => { + if (sightId !== undefined) { + return options.apiUrls.sights + '/' + sightId; + } + return options.apiUrls.sights; + }; + const getSightPath = ( getOptions: GetSightPathOptions, callback?: RequestCallback @@ -98,13 +105,6 @@ export function create(options: CreateOptions): SightsApi { }); }; - const buildUrl = (sightId?: number | string) => { - if (sightId !== undefined) { - return options.apiUrls.sights + '/' + sightId; - } - return options.apiUrls.sights; - }; - return { listSights, getSight, diff --git a/lib/sights/types.ts b/lib/sights/types.ts index 5c13987f..0feee178 100644 --- a/lib/sights/types.ts +++ b/lib/sights/types.ts @@ -2,6 +2,7 @@ import type { RequestCallback } from '../types/RequestCallback'; import type { RequestOptions } from '../types/RequestOptions'; import type { BaseResponseStatus } from '../types/BaseResponseStatus'; import type { APIAccessLevel } from '../types/ApiAccessLevel'; +import type { PathLeaf } from '../types/PathLeaf'; import type { TokenPaginationQueryParameters, TokenPaginationResponse } from '../types'; // ============================================================================ @@ -609,33 +610,24 @@ export interface SetSightPublishStatusResponse extends BaseResponseStatus { // Get Sight Path // ============================================================================ -export interface SightPathLeaf { - id: number; - name?: string; - permalink?: string; - accessLevel?: string; - createdAt?: string; - modifiedAt?: string; -} - export class SightPathNode { id: number; - name?: string; - permalink?: string; - accessLevel?: string; + name: string; + permalink: string; + accessLevel?: APIAccessLevel; folders?: SightPathNode[]; - sights?: SightPathLeaf[]; + sights?: PathLeaf[]; constructor(data: Record) { this.id = data.id as number; - this.name = data.name as string | undefined; - this.permalink = data.permalink as string | undefined; - this.accessLevel = data.accessLevel as string | undefined; + this.name = data.name as string; + this.permalink = data.permalink as string; + this.accessLevel = data.accessLevel as APIAccessLevel | undefined; if (Array.isArray(data.folders)) { this.folders = (data.folders as Record[]).map((f) => new SightPathNode(f)); } if (Array.isArray(data.sights)) { - this.sights = data.sights as SightPathLeaf[]; + this.sights = data.sights as PathLeaf[]; } } @@ -647,8 +639,8 @@ export class SightPathNode { return [this]; } - /** Returns the target SightPathLeaf sight, or undefined if not reachable. */ - getSight(): SightPathLeaf | undefined { + /** Returns the target PathLeaf sight, or undefined if not reachable. */ + getSight(): PathLeaf | undefined { for (const node of this.walkToLeaf()) { if (node.sights && node.sights.length > 0) return node.sights[0]; } diff --git a/lib/types/PathLeaf.ts b/lib/types/PathLeaf.ts new file mode 100644 index 00000000..23052fc1 --- /dev/null +++ b/lib/types/PathLeaf.ts @@ -0,0 +1,8 @@ +export interface PathLeaf { + id: number; + name: string; + permalink: string; + accessLevel: string; + createdAt: string; + modifiedAt: string; +} diff --git a/lib/types/index.ts b/lib/types/index.ts index 59bd2d4c..a66996a0 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -7,6 +7,7 @@ export * from './CreateClientOptions'; export * from './CreateOptions'; export * from './FailedItem'; export * from './ObjectValue'; +export * from './PathLeaf'; export * from './RequestCallback'; export * from './RequestOptions'; export * from './SmartsheetClient'; From b59f546307d6308ef6924f40bb18dea4d0af1d4f Mon Sep 17 00:00:00 2001 From: Andrey Strinski Date: Fri, 12 Jun 2026 11:36:27 +0300 Subject: [PATCH 06/11] Update the helper methods to use recursion --- lib/folders/types.ts | 30 ++++++------- lib/reports/types.ts | 40 +++++++++--------- lib/sheets/types.ts | 42 ++++++++++--------- lib/sights/types.ts | 40 +++++++++--------- lib/types/PathLeaf.ts | 4 +- test/mock-api/folders/get_folder_path.spec.ts | 6 +-- test/mock-api/reports/get_report_path.spec.ts | 4 +- test/mock-api/sheets/get_sheet_path.spec.ts | 4 +- test/mock-api/sights/get_sight_path.spec.ts | 4 +- 9 files changed, 91 insertions(+), 83 deletions(-) diff --git a/lib/folders/types.ts b/lib/folders/types.ts index 63931071..74fcf9e0 100644 --- a/lib/folders/types.ts +++ b/lib/folders/types.ts @@ -758,26 +758,26 @@ export class FolderPathNode { } } - private _walkToLeaf(): FolderPathNode[] { + /** Returns the deepest FolderPathNode (the target folder). */ + getLeafFolder(): FolderPathNode { if (this.folders && this.folders.length > 0) { - return [this, ...this.folders[0]._walkToLeaf()]; + return this.folders[0].getLeafFolder(); } - return [this]; - } - /** Returns the deepest FolderPathNode (the target folder). */ - getFolder(): FolderPathNode { - const nodes = this._walkToLeaf(); - return nodes[nodes.length - 1]; + return this; } - /** Returns a slash-separated path string of folder names from this node to the target folder. */ - getFolderPath(): string { - const nodes = this._walkToLeaf(); - return nodes - .map((n) => n.name) - .filter(Boolean) - .join('/'); + /** + * Returns a Unix-like slash-separated path string from this node to the target folder. + * + * @example '/Workspace/Folder/Subfolder' + **/ + getLeafFolderPath(): string { + if (this.folders && this.folders.length > 0) { + return `/${this.name}${this.folders[0].getLeafFolderPath()}`; + } + + return `/${this.name}`; } } diff --git a/lib/reports/types.ts b/lib/reports/types.ts index e36522d9..ce5872fd 100644 --- a/lib/reports/types.ts +++ b/lib/reports/types.ts @@ -1686,32 +1686,34 @@ export class ReportPathNode { } } - private _walkToLeaf(): ReportPathNode[] { - if (this.reports && this.reports.length > 0) return [this]; - if (this.folders && this.folders.length > 0) { - return [this, ...this.folders[0]._walkToLeaf()]; + /** Returns the target PathLeaf report, or undefined if not reachable. */ + getLeafReport(): PathLeaf | undefined { + if (this.reports && this.reports.length > 0) { + return this.reports[0]; } - return [this]; - } - /** Returns the target PathLeaf report, or undefined if not reachable. */ - getReport(): PathLeaf | undefined { - for (const node of this._walkToLeaf()) { - if (node.reports && node.reports.length > 0) return node.reports[0]; + if (this.folders && this.folders.length > 0) { + return this.folders[0].getLeafReport(); } + return undefined; } - /** Returns a slash-separated path string from this node to the target report. */ - getReportPath(): string | undefined { - const nodes = this._walkToLeaf(); - if (nodes.length === 0) return undefined; - const parts = nodes.map((n) => n.name).filter(Boolean) as string[]; - const leaf = nodes[nodes.length - 1]; - if (leaf.reports && leaf.reports.length > 0 && leaf.reports[0].name) { - parts[parts.length - 1] = leaf.reports[0].name; + /** + * Returns a Unix-like slash-separated path string from this node to the target report. + * + * @example '/Workspace/Folder/Report' + **/ + getLeafReportPath(): string | undefined { + if (this.reports && this.reports.length > 0) { + return `/${this.reports[0].name}`; + } + + if (this.folders && this.folders.length > 0) { + return `/${this.name}${this.folders[0].getLeafReportPath()}`; } - return parts.length > 0 ? parts.join('/') : undefined; + + return undefined; } } diff --git a/lib/sheets/types.ts b/lib/sheets/types.ts index 1f948a6d..5c7e90fd 100644 --- a/lib/sheets/types.ts +++ b/lib/sheets/types.ts @@ -9,7 +9,7 @@ export class SheetPathNode { id: number; name: string; permalink: string; - accessLevel?: string; + accessLevel?: APIAccessLevel; folders?: SheetPathNode[]; sheets?: PathLeaf[]; @@ -26,32 +26,34 @@ export class SheetPathNode { } } - private _walkToLeaf(): SheetPathNode[] { - if (this.sheets && this.sheets.length > 0) return [this]; - if (this.folders && this.folders.length > 0) { - return [this, ...this.folders[0]._walkToLeaf()]; + /** Returns the target PathLeaf sheet, or undefined if not reachable. */ + getLeafSheet(): PathLeaf | undefined { + if (this.sheets && this.sheets.length > 0) { + return this.sheets[0]; } - return [this]; - } - /** Returns the target PathLeaf sheet, or undefined if not reachable. */ - getSheet(): PathLeaf | undefined { - for (const node of this._walkToLeaf()) { - if (node.sheets && node.sheets.length > 0) return node.sheets[0]; + if (this.folders && this.folders.length > 0) { + return this.folders[0].getLeafSheet(); } + return undefined; } - /** Returns a slash-separated path string from this node to the target sheet. */ - getSheetPath(): string | undefined { - const nodes = this._walkToLeaf(); - if (nodes.length === 0) return undefined; - const parts = nodes.map((n) => n.name).filter(Boolean) as string[]; - const leaf = nodes[nodes.length - 1]; - if (leaf.sheets && leaf.sheets.length > 0 && leaf.sheets[0].name) { - parts[parts.length - 1] = leaf.sheets[0].name; + /** + * Returns a Unix-like slash-separated path string from this node to the target sheet. + * + * @example '/Workspace/Folder/Sheet' + **/ + getLeafSheetPath(): string | undefined { + if (this.sheets && this.sheets.length > 0) { + return `/${this.sheets[0].name}`; + } + + if (this.folders && this.folders.length > 0) { + return `/${this.name}${this.folders[0].getLeafSheetPath()}`; } - return parts.length > 0 ? parts.join('/') : undefined; + + return undefined; } } diff --git a/lib/sights/types.ts b/lib/sights/types.ts index 0feee178..ac09a6ec 100644 --- a/lib/sights/types.ts +++ b/lib/sights/types.ts @@ -631,32 +631,34 @@ export class SightPathNode { } } - private walkToLeaf(): SightPathNode[] { - if (this.sights && this.sights.length > 0) return [this]; - if (this.folders && this.folders.length > 0) { - return [this, ...this.folders[0].walkToLeaf()]; + /** Returns the target PathLeaf sight, or undefined if not reachable. */ + getLeafSight(): PathLeaf | undefined { + if (this.sights && this.sights.length > 0) { + return this.sights[0]; } - return [this]; - } - /** Returns the target PathLeaf sight, or undefined if not reachable. */ - getSight(): PathLeaf | undefined { - for (const node of this.walkToLeaf()) { - if (node.sights && node.sights.length > 0) return node.sights[0]; + if (this.folders && this.folders.length > 0) { + return this.folders[0].getLeafSight(); } + return undefined; } - /** Returns a slash-separated path string from this node to the target sight. */ - getSightPath(): string | undefined { - const nodes = this.walkToLeaf(); - if (nodes.length === 0) return undefined; - const parts = nodes.map((n) => n.name).filter(Boolean) as string[]; - const leaf = nodes[nodes.length - 1]; - if (leaf.sights && leaf.sights.length > 0 && leaf.sights[0].name) { - parts[parts.length - 1] = leaf.sights[0].name; + /** + * Returns a Unix-like slash-separated path string from this node to the target sight. + * + * @example '/Workspace/Folder/Dashboard' + **/ + getLeafSightPath(): string | undefined { + if (this.sights && this.sights.length > 0) { + return `/${this.sights[0].name}`; + } + + if (this.folders && this.folders.length > 0) { + return `/${this.name}${this.folders[0].getLeafSightPath()}`; } - return parts.length > 0 ? parts.join('/') : undefined; + + return undefined; } } diff --git a/lib/types/PathLeaf.ts b/lib/types/PathLeaf.ts index 23052fc1..9d57e59a 100644 --- a/lib/types/PathLeaf.ts +++ b/lib/types/PathLeaf.ts @@ -1,8 +1,10 @@ +import type { APIAccessLevel } from '@smartsheet/types'; + export interface PathLeaf { id: number; name: string; permalink: string; - accessLevel: string; + accessLevel: APIAccessLevel; createdAt: string; modifiedAt: string; } diff --git a/test/mock-api/folders/get_folder_path.spec.ts b/test/mock-api/folders/get_folder_path.spec.ts index 60ef956e..2819d954 100644 --- a/test/mock-api/folders/get_folder_path.spec.ts +++ b/test/mock-api/folders/get_folder_path.spec.ts @@ -117,13 +117,13 @@ describe('Folders - getFolderPath endpoint tests', () => { const response = await client.folders.getFolderPath(options); expect(response).toBeInstanceOf(FolderPathNode); - expect(response.getFolder()).toEqual({ + expect(response.getLeafFolder()).toEqual({ id: 3456789012345678, name: 'Project Plans Sub-Subfolder', permalink: 'https://app.smartsheet.com/folders/3456789012345678', }); - expect(response.getFolderPath()).toEqual( - 'Sample Workspace/Project Plans/Project Plans Subfolder/Project Plans Sub-Subfolder' + expect(response.getLeafFolderPath()).toEqual( + '/Sample Workspace/Project Plans/Project Plans Subfolder/Project Plans Sub-Subfolder' ); }); diff --git a/test/mock-api/reports/get_report_path.spec.ts b/test/mock-api/reports/get_report_path.spec.ts index d7fe85c0..005f1cbb 100644 --- a/test/mock-api/reports/get_report_path.spec.ts +++ b/test/mock-api/reports/get_report_path.spec.ts @@ -125,7 +125,7 @@ describe('Reports - getReportPath endpoint tests', () => { const response = await client.reports.getReportPath(options); expect(response).toBeInstanceOf(ReportPathNode); - expect(response.getReport()).toEqual({ + expect(response.getLeafReport()).toEqual({ id: 3456789012345678, name: 'Project Report', permalink: 'https://app.smartsheet.com/reports/3456789012345678', @@ -133,7 +133,7 @@ describe('Reports - getReportPath endpoint tests', () => { createdAt: TEST_REPORT_CREATED_AT, modifiedAt: TEST_REPORT_MODIFIED_AT, }); - expect(response.getReportPath()).toEqual('Sample Workspace/Project Plans/Project Report'); + expect(response.getLeafReportPath()).toEqual('/Sample Workspace/Project Plans/Project Report'); }); it('getReportPath error 500 response', async () => { diff --git a/test/mock-api/sheets/get_sheet_path.spec.ts b/test/mock-api/sheets/get_sheet_path.spec.ts index 45da791c..4a6ac71f 100644 --- a/test/mock-api/sheets/get_sheet_path.spec.ts +++ b/test/mock-api/sheets/get_sheet_path.spec.ts @@ -125,7 +125,7 @@ describe('Sheets - getSheetPath endpoint tests', () => { const response = await client.sheets.getSheetPath(options); expect(response).toBeInstanceOf(SheetPathNode); - expect(response.getSheet()).toEqual({ + expect(response.getLeafSheet()).toEqual({ id: 3456789012345678, name: 'Project Plan', permalink: 'https://app.smartsheet.com/sheets/3456789012345678', @@ -133,7 +133,7 @@ describe('Sheets - getSheetPath endpoint tests', () => { createdAt: TEST_SHEET_CREATED_AT, modifiedAt: TEST_SHEET_MODIFIED_AT, }); - expect(response.getSheetPath()).toEqual('Sample Workspace/Project Plans/Project Plan'); + expect(response.getLeafSheetPath()).toEqual('/Sample Workspace/Project Plans/Project Plan'); }); it('getSheetPath error 500 response', async () => { diff --git a/test/mock-api/sights/get_sight_path.spec.ts b/test/mock-api/sights/get_sight_path.spec.ts index e2962e1e..b00aa870 100644 --- a/test/mock-api/sights/get_sight_path.spec.ts +++ b/test/mock-api/sights/get_sight_path.spec.ts @@ -125,7 +125,7 @@ describe('Sights - getSightPath endpoint tests', () => { const response = await client.sights.getSightPath(options); expect(response).toBeInstanceOf(SightPathNode); - expect(response.getSight()).toEqual({ + expect(response.getLeafSight()).toEqual({ id: 3456789012345678, name: 'Project Dashboard', permalink: 'https://app.smartsheet.com/dashboards/3456789012345678', @@ -133,7 +133,7 @@ describe('Sights - getSightPath endpoint tests', () => { createdAt: TEST_SIGHT_CREATED_AT, modifiedAt: TEST_SIGHT_MODIFIED_AT, }); - expect(response.getSightPath()).toEqual('Sample Workspace/Project Plans/Project Dashboard'); + expect(response.getLeafSightPath()).toEqual('/Sample Workspace/Project Plans/Project Dashboard'); }); it('getSightPath error 500 response', async () => { From ad90427fc1bdcc3e7c1ab4780c8f208fa46c2e15 Mon Sep 17 00:00:00 2001 From: Andrey Strinski Date: Fri, 12 Jun 2026 11:38:25 +0300 Subject: [PATCH 07/11] Fix lint errors --- lib/folders/types.ts | 2 +- lib/reports/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/folders/types.ts b/lib/folders/types.ts index 74fcf9e0..3a9452ce 100644 --- a/lib/folders/types.ts +++ b/lib/folders/types.ts @@ -2,7 +2,7 @@ import type { RequestCallback } from '../types/RequestCallback'; import type { RequestOptions } from '../types/RequestOptions'; import type { BaseResponseStatus } from '../types/BaseResponseStatus'; import type { FailedItem } from '../types/FailedItem'; -import { APIAccessLevel } from '@smartsheet/types'; +import type { APIAccessLevel } from '@smartsheet/types'; // ============================================================================ // Folders API Interface diff --git a/lib/reports/types.ts b/lib/reports/types.ts index ce5872fd..ac0e7203 100644 --- a/lib/reports/types.ts +++ b/lib/reports/types.ts @@ -11,7 +11,7 @@ import type { CrossSheetReference } from '../cross-sheet-references/types'; import type { SheetSummary } from '../sheet-summary/types'; import type { SheetUserSettings } from '../sheets/types'; import type { WorkspaceListing } from '../workspaces/types'; -import { APIAccessLevel } from '@smartsheet/types'; +import type { APIAccessLevel } from '@smartsheet/types'; // ============================================================================ // Reports API Interface From 3f8b8ab89e08ab3697d20c6b7e1177cf5958834b Mon Sep 17 00:00:00 2001 From: Andrey Strinski Date: Fri, 12 Jun 2026 11:42:43 +0300 Subject: [PATCH 08/11] Fix build --- lib/folders/types.ts | 2 +- lib/reports/types.ts | 2 +- lib/sheets/types.ts | 2 +- lib/types/PathLeaf.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/folders/types.ts b/lib/folders/types.ts index 3a9452ce..2b9d4c8f 100644 --- a/lib/folders/types.ts +++ b/lib/folders/types.ts @@ -2,7 +2,7 @@ import type { RequestCallback } from '../types/RequestCallback'; import type { RequestOptions } from '../types/RequestOptions'; import type { BaseResponseStatus } from '../types/BaseResponseStatus'; import type { FailedItem } from '../types/FailedItem'; -import type { APIAccessLevel } from '@smartsheet/types'; +import type { APIAccessLevel } from '../types/ApiAccessLevel'; // ============================================================================ // Folders API Interface diff --git a/lib/reports/types.ts b/lib/reports/types.ts index ac0e7203..c8df5049 100644 --- a/lib/reports/types.ts +++ b/lib/reports/types.ts @@ -11,7 +11,7 @@ import type { CrossSheetReference } from '../cross-sheet-references/types'; import type { SheetSummary } from '../sheet-summary/types'; import type { SheetUserSettings } from '../sheets/types'; import type { WorkspaceListing } from '../workspaces/types'; -import type { APIAccessLevel } from '@smartsheet/types'; +import type { APIAccessLevel } from '../types/ApiAccessLevel'; // ============================================================================ // Reports API Interface diff --git a/lib/sheets/types.ts b/lib/sheets/types.ts index 5c7e90fd..8e632683 100644 --- a/lib/sheets/types.ts +++ b/lib/sheets/types.ts @@ -3,7 +3,7 @@ // ============================================================================ import type { PathLeaf } from '../types/PathLeaf'; -import type { APIAccessLevel } from '@smartsheet/types'; +import type { APIAccessLevel } from '../types/ApiAccessLevel'; export class SheetPathNode { id: number; diff --git a/lib/types/PathLeaf.ts b/lib/types/PathLeaf.ts index 9d57e59a..fba13fa5 100644 --- a/lib/types/PathLeaf.ts +++ b/lib/types/PathLeaf.ts @@ -1,4 +1,4 @@ -import type { APIAccessLevel } from '@smartsheet/types'; +import type { APIAccessLevel } from './ApiAccessLevel'; export interface PathLeaf { id: number; From ff7b344cde1fa49163e3ba09f7bd513e310cb4c8 Mon Sep 17 00:00:00 2001 From: Andrey Strinski Date: Mon, 15 Jun 2026 16:13:47 +0300 Subject: [PATCH 09/11] Moved away from classes to interfaces --- lib/folders/index.ts | 9 ++- lib/folders/types.ts | 54 ++++++--------- lib/reports/index.ts | 9 ++- lib/reports/types.ts | 69 ++++++++----------- lib/sheets/index.js | 6 +- lib/sheets/types.ts | 69 ++++++++----------- lib/sights/index.ts | 9 ++- lib/sights/types.ts | 68 ++++++++---------- lib/types/PathLeaf.ts | 9 +-- lib/types/PathNode.ts | 8 +++ lib/types/index.ts | 1 + test/mock-api/folders/get_folder_path.spec.ts | 9 ++- test/mock-api/reports/get_report_path.spec.ts | 9 ++- test/mock-api/sheets/get_sheet_path.spec.ts | 9 ++- test/mock-api/sights/get_sight_path.spec.ts | 9 ++- 15 files changed, 148 insertions(+), 199 deletions(-) create mode 100644 lib/types/PathNode.ts diff --git a/lib/folders/index.ts b/lib/folders/index.ts index 0aa8d705..2bf963fb 100644 --- a/lib/folders/index.ts +++ b/lib/folders/index.ts @@ -19,7 +19,7 @@ import type { CopyFolderResponse, GetFolderPathOptions, } from './types'; -import { FolderPathNode } from './types'; +import type { FolderPathNode } from './types'; export function create(options: CreateOptions): FoldersApi { const requestor = options.requestor; @@ -93,10 +93,9 @@ export function create(options: CreateOptions): FoldersApi { const urlOptions = { url: options.apiUrls.folders + '/' + getOptions.folderId + '/path' }; return requestor .get({ ...optionsToSend, ...urlOptions, ...getOptions }) - .then((data: Record) => { - const node = new FolderPathNode(data); - if (callback) callback(undefined, node); - return node; + .then((data: FolderPathNode) => { + if (callback) callback(undefined, data); + return data; }) .catch((err: unknown) => { if (callback) callback(err as ApiError, undefined); diff --git a/lib/folders/types.ts b/lib/folders/types.ts index 2b9d4c8f..82383cfa 100644 --- a/lib/folders/types.ts +++ b/lib/folders/types.ts @@ -2,7 +2,7 @@ import type { RequestCallback } from '../types/RequestCallback'; import type { RequestOptions } from '../types/RequestOptions'; import type { BaseResponseStatus } from '../types/BaseResponseStatus'; import type { FailedItem } from '../types/FailedItem'; -import type { APIAccessLevel } from '../types/ApiAccessLevel'; +import type { PathNode } from '../types/PathNode'; // ============================================================================ // Folders API Interface @@ -741,44 +741,34 @@ export interface MoveFolderResponse extends BaseResponseStatus { // Get Folder Path // ============================================================================ -export class FolderPathNode { - id: number; - name: string; - permalink: string; - accessLevel?: APIAccessLevel; +export interface FolderPathNode extends PathNode { folders?: FolderPathNode[]; +} - constructor(data: Record) { - this.id = data.id as number; - this.name = data.name as string; - this.permalink = data.permalink as string; - this.accessLevel = data.accessLevel as APIAccessLevel | undefined; - if (Array.isArray(data.folders)) { - this.folders = (data.folders as Record[]).map((f) => new FolderPathNode(f)); - } +/** + * Returns the deepest {@link FolderPathNode} (the target folder). + * Use this for quick access to the leaf folder object from a path response. + */ +export function getLeafFolder(node: FolderPathNode): FolderPathNode { + if (node.folders && node.folders.length > 0) { + return getLeafFolder(node.folders[0]); } - /** Returns the deepest FolderPathNode (the target folder). */ - getLeafFolder(): FolderPathNode { - if (this.folders && this.folders.length > 0) { - return this.folders[0].getLeafFolder(); - } + return node; +} - return this; +/** + * Returns a Unix-like slash-separated path string from the given node to the target folder. + * Use this for quick access to the full path string of the leaf folder from a path response. + * + * @example '/Workspace/Folder/Subfolder' + */ +export function getLeafFolderPath(node: FolderPathNode): string { + if (node.folders && node.folders.length > 0) { + return `/${node.name}${getLeafFolderPath(node.folders[0])}`; } - /** - * Returns a Unix-like slash-separated path string from this node to the target folder. - * - * @example '/Workspace/Folder/Subfolder' - **/ - getLeafFolderPath(): string { - if (this.folders && this.folders.length > 0) { - return `/${this.name}${this.folders[0].getLeafFolderPath()}`; - } - - return `/${this.name}`; - } + return `/${node.name}`; } export interface GetFolderPathOptions extends RequestOptions { diff --git a/lib/reports/index.ts b/lib/reports/index.ts index c600832b..4462f9b9 100644 --- a/lib/reports/index.ts +++ b/lib/reports/index.ts @@ -28,7 +28,7 @@ import type { CreateReportResponse, GetReportPathOptions, } from './types'; -import { ReportPathNode } from './types'; +import type { ReportPathNode } from './types'; import * as constants from '../utils/constants'; export function create(options: CreateOptions): ReportsApi { @@ -150,10 +150,9 @@ export function create(options: CreateOptions): ReportsApi { const urlOptions = { url: options.apiUrls.reports + '/' + getOptions.reportId + '/path' }; return requestor .get({ ...optionsToSend, ...urlOptions, ...getOptions }) - .then((data: Record) => { - const node = new ReportPathNode(data); - if (callback) callback(undefined, node); - return node; + .then((data: ReportPathNode) => { + if (callback) callback(undefined, data); + return data; }) .catch((err: unknown) => { if (callback) callback(err as ApiError, undefined); diff --git a/lib/reports/types.ts b/lib/reports/types.ts index c8df5049..4abee279 100644 --- a/lib/reports/types.ts +++ b/lib/reports/types.ts @@ -1,4 +1,5 @@ import type { PathLeaf } from '../types/PathLeaf'; +import type { PathNode } from '../types/PathNode'; import type { RequestCallback } from '../types/RequestCallback'; import type { RequestOptions } from '../types/RequestOptions'; import type { BaseResponseStatus } from '../types/BaseResponseStatus'; @@ -11,7 +12,6 @@ import type { CrossSheetReference } from '../cross-sheet-references/types'; import type { SheetSummary } from '../sheet-summary/types'; import type { SheetUserSettings } from '../sheets/types'; import type { WorkspaceListing } from '../workspaces/types'; -import type { APIAccessLevel } from '../types/ApiAccessLevel'; // ============================================================================ // Reports API Interface @@ -1665,56 +1665,43 @@ export interface CreateReportResponse extends BaseResponseStatus { // Get Report Path // ============================================================================ -export class ReportPathNode { - id: number; - name: string; - permalink: string; - accessLevel?: APIAccessLevel; +export interface ReportPathNode extends PathNode { folders?: ReportPathNode[]; reports?: PathLeaf[]; +} - constructor(data: Record) { - this.id = data.id as number; - this.name = data.name as string; - this.permalink = data.permalink as string; - this.accessLevel = data.accessLevel as APIAccessLevel | undefined; - if (Array.isArray(data.folders)) { - this.folders = (data.folders as Record[]).map((f) => new ReportPathNode(f)); - } - if (Array.isArray(data.reports)) { - this.reports = data.reports as PathLeaf[]; - } +/** + * Returns the target {@link PathLeaf} report, or undefined if not reachable. + * Use this for quick access to the leaf report object from a path response. + */ +export function getLeafReport(node: ReportPathNode): PathLeaf | undefined { + if (node.reports && node.reports.length > 0) { + return node.reports[0]; } - /** Returns the target PathLeaf report, or undefined if not reachable. */ - getLeafReport(): PathLeaf | undefined { - if (this.reports && this.reports.length > 0) { - return this.reports[0]; - } - - if (this.folders && this.folders.length > 0) { - return this.folders[0].getLeafReport(); - } - - return undefined; + if (node.folders && node.folders.length > 0) { + return getLeafReport(node.folders[0]); } - /** - * Returns a Unix-like slash-separated path string from this node to the target report. - * - * @example '/Workspace/Folder/Report' - **/ - getLeafReportPath(): string | undefined { - if (this.reports && this.reports.length > 0) { - return `/${this.reports[0].name}`; - } + return undefined; +} - if (this.folders && this.folders.length > 0) { - return `/${this.name}${this.folders[0].getLeafReportPath()}`; - } +/** + * Returns a Unix-like slash-separated path string from the given node to the target report. + * Use this for quick access to the full path string of the leaf report from a path response. + * + * @example '/Workspace/Folder/Report' + */ +export function getLeafReportPath(node: ReportPathNode): string | undefined { + if (node.reports && node.reports.length > 0) { + return `/${node.reports[0].name}`; + } - return undefined; + if (node.folders && node.folders.length > 0) { + return `/${node.name}${getLeafReportPath(node.folders[0])}`; } + + return undefined; } export interface GetReportPathOptions extends RequestOptions { diff --git a/lib/sheets/index.js b/lib/sheets/index.js index 10593c96..f8e34553 100644 --- a/lib/sheets/index.js +++ b/lib/sheets/index.js @@ -1,5 +1,4 @@ import _ from 'underscore'; -import { SheetPathNode } from './types'; import * as attachments from './attachments'; import * as automationRules from './automationrules'; import * as columns from './columns'; @@ -61,9 +60,8 @@ export function create(options) { return requestor .get(_.extend({}, optionsToSend, urlOptions, getOptions)) .then((data) => { - const node = new SheetPathNode(data); - if (callback) callback(undefined, node); - return node; + if (callback) callback(undefined, data); + return data; }) .catch((err) => { if (callback) callback(err, undefined); diff --git a/lib/sheets/types.ts b/lib/sheets/types.ts index 8e632683..e24fd047 100644 --- a/lib/sheets/types.ts +++ b/lib/sheets/types.ts @@ -3,58 +3,45 @@ // ============================================================================ import type { PathLeaf } from '../types/PathLeaf'; -import type { APIAccessLevel } from '../types/ApiAccessLevel'; +import type { PathNode } from '../types/PathNode'; -export class SheetPathNode { - id: number; - name: string; - permalink: string; - accessLevel?: APIAccessLevel; +export interface SheetPathNode extends PathNode { folders?: SheetPathNode[]; sheets?: PathLeaf[]; +} - constructor(data: Record) { - this.id = data.id as number; - this.name = data.name as string; - this.permalink = data.permalink as string; - this.accessLevel = data.accessLevel as APIAccessLevel | undefined; - if (Array.isArray(data.folders)) { - this.folders = (data.folders as Record[]).map((f) => new SheetPathNode(f)); - } - if (Array.isArray(data.sheets)) { - this.sheets = data.sheets as PathLeaf[]; - } +/** + * Returns the target {@link PathLeaf} sheet, or undefined if not reachable. + * Use this for quick access to the leaf sheet object from a path response. + */ +export function getLeafSheet(node: SheetPathNode): PathLeaf | undefined { + if (node.sheets && node.sheets.length > 0) { + return node.sheets[0]; } - /** Returns the target PathLeaf sheet, or undefined if not reachable. */ - getLeafSheet(): PathLeaf | undefined { - if (this.sheets && this.sheets.length > 0) { - return this.sheets[0]; - } - - if (this.folders && this.folders.length > 0) { - return this.folders[0].getLeafSheet(); - } - - return undefined; + if (node.folders && node.folders.length > 0) { + return getLeafSheet(node.folders[0]); } - /** - * Returns a Unix-like slash-separated path string from this node to the target sheet. - * - * @example '/Workspace/Folder/Sheet' - **/ - getLeafSheetPath(): string | undefined { - if (this.sheets && this.sheets.length > 0) { - return `/${this.sheets[0].name}`; - } + return undefined; +} - if (this.folders && this.folders.length > 0) { - return `/${this.name}${this.folders[0].getLeafSheetPath()}`; - } +/** + * Returns a Unix-like slash-separated path string from the given node to the target sheet. + * Use this for quick access to the full path string of the leaf sheet from a path response. + * + * @example '/Workspace/Folder/Sheet' + */ +export function getLeafSheetPath(node: SheetPathNode): string | undefined { + if (node.sheets && node.sheets.length > 0) { + return `/${node.sheets[0].name}`; + } - return undefined; + if (node.folders && node.folders.length > 0) { + return `/${node.name}${getLeafSheetPath(node.folders[0])}`; } + + return undefined; } export interface SheetUserSettings { diff --git a/lib/sights/index.ts b/lib/sights/index.ts index d71de54e..bd01974f 100644 --- a/lib/sights/index.ts +++ b/lib/sights/index.ts @@ -22,7 +22,7 @@ import type { SetSightPublishStatusResponse, GetSightPathOptions, } from './types'; -import { SightPathNode } from './types'; +import type { SightPathNode } from './types'; export function create(options: CreateOptions): SightsApi { const requestor = options.requestor; @@ -94,10 +94,9 @@ export function create(options: CreateOptions): SightsApi { const urlOptions = { url: buildUrl(getOptions.sightId) + '/path' }; return requestor .get({ ...optionsToSend, ...urlOptions, ...getOptions }) - .then((data: Record) => { - const node = new SightPathNode(data); - if (callback) callback(undefined, node); - return node; + .then((data: SightPathNode) => { + if (callback) callback(undefined, data); + return data; }) .catch((err: unknown) => { if (callback) callback(err as ApiError, undefined); diff --git a/lib/sights/types.ts b/lib/sights/types.ts index ac09a6ec..808bc78a 100644 --- a/lib/sights/types.ts +++ b/lib/sights/types.ts @@ -3,6 +3,7 @@ import type { RequestOptions } from '../types/RequestOptions'; import type { BaseResponseStatus } from '../types/BaseResponseStatus'; import type { APIAccessLevel } from '../types/ApiAccessLevel'; import type { PathLeaf } from '../types/PathLeaf'; +import type { PathNode } from '../types/PathNode'; import type { TokenPaginationQueryParameters, TokenPaginationResponse } from '../types'; // ============================================================================ @@ -610,56 +611,43 @@ export interface SetSightPublishStatusResponse extends BaseResponseStatus { // Get Sight Path // ============================================================================ -export class SightPathNode { - id: number; - name: string; - permalink: string; - accessLevel?: APIAccessLevel; +export interface SightPathNode extends PathNode { folders?: SightPathNode[]; sights?: PathLeaf[]; +} - constructor(data: Record) { - this.id = data.id as number; - this.name = data.name as string; - this.permalink = data.permalink as string; - this.accessLevel = data.accessLevel as APIAccessLevel | undefined; - if (Array.isArray(data.folders)) { - this.folders = (data.folders as Record[]).map((f) => new SightPathNode(f)); - } - if (Array.isArray(data.sights)) { - this.sights = data.sights as PathLeaf[]; - } +/** + * Returns the target {@link PathLeaf} sight, or undefined if not reachable. + * Use this for quick access to the leaf sight object from a path response. + */ +export function getLeafSight(node: SightPathNode): PathLeaf | undefined { + if (node.sights && node.sights.length > 0) { + return node.sights[0]; } - /** Returns the target PathLeaf sight, or undefined if not reachable. */ - getLeafSight(): PathLeaf | undefined { - if (this.sights && this.sights.length > 0) { - return this.sights[0]; - } + if (node.folders && node.folders.length > 0) { + return getLeafSight(node.folders[0]); + } - if (this.folders && this.folders.length > 0) { - return this.folders[0].getLeafSight(); - } + return undefined; +} - return undefined; +/** + * Returns a Unix-like slash-separated path string from the given node to the target sight. + * Use this for quick access to the full path string of the leaf sight from a path response. + * + * @example '/Workspace/Folder/Dashboard' + */ +export function getLeafSightPath(node: SightPathNode): string | undefined { + if (node.sights && node.sights.length > 0) { + return `/${node.sights[0].name}`; } - /** - * Returns a Unix-like slash-separated path string from this node to the target sight. - * - * @example '/Workspace/Folder/Dashboard' - **/ - getLeafSightPath(): string | undefined { - if (this.sights && this.sights.length > 0) { - return `/${this.sights[0].name}`; - } - - if (this.folders && this.folders.length > 0) { - return `/${this.name}${this.folders[0].getLeafSightPath()}`; - } - - return undefined; + if (node.folders && node.folders.length > 0) { + return `/${node.name}${getLeafSightPath(node.folders[0])}`; } + + return undefined; } export interface GetSightPathOptions extends RequestOptions { diff --git a/lib/types/PathLeaf.ts b/lib/types/PathLeaf.ts index fba13fa5..967d24d5 100644 --- a/lib/types/PathLeaf.ts +++ b/lib/types/PathLeaf.ts @@ -1,10 +1,7 @@ -import type { APIAccessLevel } from './ApiAccessLevel'; +import type { PathNode } from './PathNode'; -export interface PathLeaf { - id: number; - name: string; - permalink: string; - accessLevel: APIAccessLevel; +export interface PathLeaf extends PathNode { + accessLevel: NonNullable; createdAt: string; modifiedAt: string; } diff --git a/lib/types/PathNode.ts b/lib/types/PathNode.ts new file mode 100644 index 00000000..b4f235f7 --- /dev/null +++ b/lib/types/PathNode.ts @@ -0,0 +1,8 @@ +import type { APIAccessLevel } from './ApiAccessLevel'; + +export interface PathNode { + id: number; + name: string; + permalink: string; + accessLevel?: APIAccessLevel; +} diff --git a/lib/types/index.ts b/lib/types/index.ts index a66996a0..7b71feb9 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -8,6 +8,7 @@ export * from './CreateOptions'; export * from './FailedItem'; export * from './ObjectValue'; export * from './PathLeaf'; +export * from './PathNode'; export * from './RequestCallback'; export * from './RequestOptions'; export * from './SmartsheetClient'; diff --git a/test/mock-api/folders/get_folder_path.spec.ts b/test/mock-api/folders/get_folder_path.spec.ts index 2819d954..ede26083 100644 --- a/test/mock-api/folders/get_folder_path.spec.ts +++ b/test/mock-api/folders/get_folder_path.spec.ts @@ -12,7 +12,7 @@ import { ERROR_400_MESSAGE, } from './common_test_constants'; import { APIAccessLevel } from '@smartsheet/types'; -import { FolderPathNode } from '@smartsheet/folders/types'; +import { getLeafFolder, getLeafFolderPath } from '@smartsheet/folders/types'; describe('Folders - getFolderPath endpoint tests', () => { const client = createClient(); @@ -105,7 +105,7 @@ describe('Folders - getFolderPath endpoint tests', () => { }); }); - it('getFolderPath returns FolderPathNode instance with getFolder and getFolderPath methods', async () => { + it('getFolderPath returns FolderPathNode compatible with getLeafFolder and getLeafFolderPath', async () => { const requestId = crypto.randomUUID(); const options = { folderId: TEST_FOLDER_ID, @@ -116,13 +116,12 @@ describe('Folders - getFolderPath endpoint tests', () => { }; const response = await client.folders.getFolderPath(options); - expect(response).toBeInstanceOf(FolderPathNode); - expect(response.getLeafFolder()).toEqual({ + expect(getLeafFolder(response)).toEqual({ id: 3456789012345678, name: 'Project Plans Sub-Subfolder', permalink: 'https://app.smartsheet.com/folders/3456789012345678', }); - expect(response.getLeafFolderPath()).toEqual( + expect(getLeafFolderPath(response)).toEqual( '/Sample Workspace/Project Plans/Project Plans Subfolder/Project Plans Sub-Subfolder' ); }); diff --git a/test/mock-api/reports/get_report_path.spec.ts b/test/mock-api/reports/get_report_path.spec.ts index 005f1cbb..31f14a32 100644 --- a/test/mock-api/reports/get_report_path.spec.ts +++ b/test/mock-api/reports/get_report_path.spec.ts @@ -2,6 +2,7 @@ import crypto from 'crypto'; import { createClient, findWireMockRequest } from '../utils/utils'; import { expect } from '@jest/globals'; import { APIAccessLevel } from '@smartsheet/types'; +import { getLeafReport, getLeafReportPath } from '@smartsheet/reports/types'; import { TEST_REPORT_ID, TEST_REPORT_CREATED_AT, @@ -14,7 +15,6 @@ import { ERROR_400_STATUS_CODE, ERROR_400_MESSAGE, } from './common_test_constants'; -import { ReportPathNode } from '@smartsheet/reports/types'; describe('Reports - getReportPath endpoint tests', () => { const client = createClient(); @@ -113,7 +113,7 @@ describe('Reports - getReportPath endpoint tests', () => { }); }); - it('getReportPath returns ReportPathNode instance with getReport and getReportPath methods', async () => { + it('getReportPath returns ReportPathNode compatible with getLeafReport and getLeafReportPath', async () => { const requestId = crypto.randomUUID(); const options = { reportId: TEST_REPORT_ID, @@ -124,8 +124,7 @@ describe('Reports - getReportPath endpoint tests', () => { }; const response = await client.reports.getReportPath(options); - expect(response).toBeInstanceOf(ReportPathNode); - expect(response.getLeafReport()).toEqual({ + expect(getLeafReport(response)).toEqual({ id: 3456789012345678, name: 'Project Report', permalink: 'https://app.smartsheet.com/reports/3456789012345678', @@ -133,7 +132,7 @@ describe('Reports - getReportPath endpoint tests', () => { createdAt: TEST_REPORT_CREATED_AT, modifiedAt: TEST_REPORT_MODIFIED_AT, }); - expect(response.getLeafReportPath()).toEqual('/Sample Workspace/Project Plans/Project Report'); + expect(getLeafReportPath(response)).toEqual('/Sample Workspace/Project Plans/Project Report'); }); it('getReportPath error 500 response', async () => { diff --git a/test/mock-api/sheets/get_sheet_path.spec.ts b/test/mock-api/sheets/get_sheet_path.spec.ts index 4a6ac71f..dea21fed 100644 --- a/test/mock-api/sheets/get_sheet_path.spec.ts +++ b/test/mock-api/sheets/get_sheet_path.spec.ts @@ -2,7 +2,7 @@ import crypto from 'crypto'; import { createClient, findWireMockRequest } from '../utils/utils'; import { expect } from '@jest/globals'; import { APIAccessLevel } from '@smartsheet/types'; -import { SheetPathNode } from '@smartsheet/sheets/types'; +import { getLeafSheet, getLeafSheetPath } from '@smartsheet/sheets/types'; import { TEST_SHEET_ID, TEST_SHEET_CREATED_AT, @@ -113,7 +113,7 @@ describe('Sheets - getSheetPath endpoint tests', () => { }); }); - it('getSheetPath returns SheetPathNode instance with getSheet and getSheetPath methods', async () => { + it('getSheetPath returns SheetPathNode compatible with getLeafSheet and getLeafSheetPath', async () => { const requestId = crypto.randomUUID(); const options = { sheetId: TEST_SHEET_ID, @@ -124,8 +124,7 @@ describe('Sheets - getSheetPath endpoint tests', () => { }; const response = await client.sheets.getSheetPath(options); - expect(response).toBeInstanceOf(SheetPathNode); - expect(response.getLeafSheet()).toEqual({ + expect(getLeafSheet(response)).toEqual({ id: 3456789012345678, name: 'Project Plan', permalink: 'https://app.smartsheet.com/sheets/3456789012345678', @@ -133,7 +132,7 @@ describe('Sheets - getSheetPath endpoint tests', () => { createdAt: TEST_SHEET_CREATED_AT, modifiedAt: TEST_SHEET_MODIFIED_AT, }); - expect(response.getLeafSheetPath()).toEqual('/Sample Workspace/Project Plans/Project Plan'); + expect(getLeafSheetPath(response)).toEqual('/Sample Workspace/Project Plans/Project Plan'); }); it('getSheetPath error 500 response', async () => { diff --git a/test/mock-api/sights/get_sight_path.spec.ts b/test/mock-api/sights/get_sight_path.spec.ts index b00aa870..2dafd4bf 100644 --- a/test/mock-api/sights/get_sight_path.spec.ts +++ b/test/mock-api/sights/get_sight_path.spec.ts @@ -2,7 +2,7 @@ import crypto from 'crypto'; import { createClient, findWireMockRequest } from '../utils/utils'; import { expect } from '@jest/globals'; import { APIAccessLevel } from '@smartsheet/types'; -import { SightPathNode } from '@smartsheet/sights/types'; +import { getLeafSight, getLeafSightPath } from '@smartsheet/sights/types'; import { TEST_SIGHT_ID, TEST_SIGHT_CREATED_AT, @@ -113,7 +113,7 @@ describe('Sights - getSightPath endpoint tests', () => { }); }); - it('getSightPath returns SightPathNode instance with getSight and getSightPath methods', async () => { + it('getSightPath returns SightPathNode compatible with getLeafSight and getLeafSightPath', async () => { const requestId = crypto.randomUUID(); const options = { sightId: TEST_SIGHT_ID, @@ -124,8 +124,7 @@ describe('Sights - getSightPath endpoint tests', () => { }; const response = await client.sights.getSightPath(options); - expect(response).toBeInstanceOf(SightPathNode); - expect(response.getLeafSight()).toEqual({ + expect(getLeafSight(response)).toEqual({ id: 3456789012345678, name: 'Project Dashboard', permalink: 'https://app.smartsheet.com/dashboards/3456789012345678', @@ -133,7 +132,7 @@ describe('Sights - getSightPath endpoint tests', () => { createdAt: TEST_SIGHT_CREATED_AT, modifiedAt: TEST_SIGHT_MODIFIED_AT, }); - expect(response.getLeafSightPath()).toEqual('/Sample Workspace/Project Plans/Project Dashboard'); + expect(getLeafSightPath(response)).toEqual('/Sample Workspace/Project Plans/Project Dashboard'); }); it('getSightPath error 500 response', async () => { From edb3bbc6c195014522d0a928c2f20d0249f0a6c2 Mon Sep 17 00:00:00 2001 From: Andrey Strinski Date: Mon, 15 Jun 2026 16:28:02 +0300 Subject: [PATCH 10/11] Fixed changelog and AI review comments --- CHANGELOG.md | 8 ++++++++ lib/folders/index.ts | 12 +----------- lib/reports/index.ts | 12 +----------- lib/sheets/index.js | 11 +---------- lib/sights/index.ts | 12 +----------- 5 files changed, 12 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8238cb7d..06692245 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [X.X.X] - Unreleased +### Added + +- Added support for GET /2.0/sheets/{sheetId}/path endpoint (`getSheetPath`) +- Added support for GET /2.0/reports/{reportId}/path endpoint (`getReportPath`) +- Added support for GET /2.0/sights/{sightId}/path endpoint (`getSightPath`) +- Added support for GET /2.0/folders/{folderId}/path endpoint (`getFolderPath`) +- Added helper functions `getLeaf()` and `getLeafPath()` for convenient traversal of the path node objects + ## [5.0.1] - 2026-06-10 ### Fixed diff --git a/lib/folders/index.ts b/lib/folders/index.ts index 2bf963fb..da44af29 100644 --- a/lib/folders/index.ts +++ b/lib/folders/index.ts @@ -1,4 +1,3 @@ -import type { ApiError } from '../types/ApiError'; import type { CreateOptions } from '../types/CreateOptions'; import type { RequestCallback } from '../types/RequestCallback'; import type { BaseResponseStatus } from '../types/BaseResponseStatus'; @@ -91,16 +90,7 @@ export function create(options: CreateOptions): FoldersApi { callback?: RequestCallback ): Promise => { const urlOptions = { url: options.apiUrls.folders + '/' + getOptions.folderId + '/path' }; - return requestor - .get({ ...optionsToSend, ...urlOptions, ...getOptions }) - .then((data: FolderPathNode) => { - if (callback) callback(undefined, data); - return data; - }) - .catch((err: unknown) => { - if (callback) callback(err as ApiError, undefined); - return Promise.reject(err); - }); + return requestor.get({ ...optionsToSend, ...urlOptions, ...getOptions }, callback); }; return { diff --git a/lib/reports/index.ts b/lib/reports/index.ts index 4462f9b9..6f743abe 100644 --- a/lib/reports/index.ts +++ b/lib/reports/index.ts @@ -1,4 +1,3 @@ -import type { ApiError } from '../types/ApiError'; import type { BaseResponseStatus } from '../types/BaseResponseStatus'; import type { CreateOptions } from '../types/CreateOptions'; import type { RequestCallback } from '../types/RequestCallback'; @@ -148,16 +147,7 @@ export function create(options: CreateOptions): ReportsApi { callback?: RequestCallback ): Promise => { const urlOptions = { url: options.apiUrls.reports + '/' + getOptions.reportId + '/path' }; - return requestor - .get({ ...optionsToSend, ...urlOptions, ...getOptions }) - .then((data: ReportPathNode) => { - if (callback) callback(undefined, data); - return data; - }) - .catch((err: unknown) => { - if (callback) callback(err as ApiError, undefined); - return Promise.reject(err); - }); + return requestor.get({ ...optionsToSend, ...urlOptions, ...getOptions }, callback); }; return { diff --git a/lib/sheets/index.js b/lib/sheets/index.js index f8e34553..38e207f1 100644 --- a/lib/sheets/index.js +++ b/lib/sheets/index.js @@ -57,16 +57,7 @@ export function create(options) { const getSheetPath = (getOptions, callback) => { const urlOptions = { url: options.apiUrls.sheets + '/' + getOptions.sheetId + '/path' }; - return requestor - .get(_.extend({}, optionsToSend, urlOptions, getOptions)) - .then((data) => { - if (callback) callback(undefined, data); - return data; - }) - .catch((err) => { - if (callback) callback(err, undefined); - return Promise.reject(err); - }); + return requestor.get(_.extend({}, optionsToSend, urlOptions, getOptions), callback); }; const sheetObject = { diff --git a/lib/sights/index.ts b/lib/sights/index.ts index bd01974f..27d6ba7f 100644 --- a/lib/sights/index.ts +++ b/lib/sights/index.ts @@ -1,4 +1,3 @@ -import type { ApiError } from '../types/ApiError'; import type { CreateOptions } from '../types/CreateOptions'; import type { RequestCallback } from '../types/RequestCallback'; import type { RequestOptions } from '../types/RequestOptions'; @@ -92,16 +91,7 @@ export function create(options: CreateOptions): SightsApi { callback?: RequestCallback ): Promise => { const urlOptions = { url: buildUrl(getOptions.sightId) + '/path' }; - return requestor - .get({ ...optionsToSend, ...urlOptions, ...getOptions }) - .then((data: SightPathNode) => { - if (callback) callback(undefined, data); - return data; - }) - .catch((err: unknown) => { - if (callback) callback(err as ApiError, undefined); - return Promise.reject(err); - }); + return requestor.get({ ...optionsToSend, ...urlOptions, ...getOptions }, callback); }; return { From 6b623ae09a215eeb1c56ba63254f86939c2c1260 Mon Sep 17 00:00:00 2001 From: Andrey Strinski Date: Tue, 16 Jun 2026 10:46:28 +0300 Subject: [PATCH 11/11] Moved helper functions into separate files and updated gocstrings --- lib/folders/helpers.ts | 45 ++++++++++++++++ lib/folders/types.ts | 34 ++++-------- lib/reports/helpers.ts | 54 +++++++++++++++++++ lib/reports/types.ts | 42 ++++----------- lib/sheets/helpers.ts | 54 +++++++++++++++++++ lib/sheets/types.ts | 42 +++------------ lib/sights/helpers.ts | 54 +++++++++++++++++++ lib/sights/types.ts | 42 ++++----------- test/mock-api/folders/get_folder_path.spec.ts | 2 +- test/mock-api/reports/get_report_path.spec.ts | 2 +- test/mock-api/sheets/get_sheet_path.spec.ts | 2 +- test/mock-api/sights/get_sight_path.spec.ts | 2 +- 12 files changed, 252 insertions(+), 123 deletions(-) create mode 100644 lib/folders/helpers.ts create mode 100644 lib/reports/helpers.ts create mode 100644 lib/sheets/helpers.ts create mode 100644 lib/sights/helpers.ts diff --git a/lib/folders/helpers.ts b/lib/folders/helpers.ts new file mode 100644 index 00000000..a6809340 --- /dev/null +++ b/lib/folders/helpers.ts @@ -0,0 +1,45 @@ +import type { FolderPathNode } from './types'; + +/** + * Returns the deepest {@link FolderPathNode} (the target folder). + * Use this for quick access to the leaf folder object from a path response. + * + * @param node - The root {@link FolderPathNode} returned by `getFolderPath` + * @returns The deepest (leaf) {@link FolderPathNode} + * + * @example + * ```typescript + * const path = await client.folders.getFolderPath({ folderId: 7116448184199044 }); + * const leaf = getLeafFolder(path); + * console.log(leaf.name); + * ``` + */ +export function getLeafFolder(node: FolderPathNode): FolderPathNode { + if (node.folders && node.folders.length > 0) { + return getLeafFolder(node.folders[0]); + } + + return node; +} + +/** + * Returns a Unix-like slash-separated path string from the given node to the target folder. + * Use this for quick access to the full path string of the leaf folder from a path response. + * + * @param node - The root {@link FolderPathNode} returned by `getFolderPath` + * @returns A slash-separated path string + * + * @example + * ```typescript + * const path = await client.folders.getFolderPath({ folderId: 7116448184199044 }); + * const pathStr = getLeafFolderPath(path); + * console.log(pathStr); // '/Workspace/Folder/Subfolder' + * ``` + */ +export function getLeafFolderPath(node: FolderPathNode): string { + if (node.folders && node.folders.length > 0) { + return `/${node.name}${getLeafFolderPath(node.folders[0])}`; + } + + return `/${node.name}`; +} diff --git a/lib/folders/types.ts b/lib/folders/types.ts index 82383cfa..27e9fb38 100644 --- a/lib/folders/types.ts +++ b/lib/folders/types.ts @@ -741,36 +741,24 @@ export interface MoveFolderResponse extends BaseResponseStatus { // Get Folder Path // ============================================================================ -export interface FolderPathNode extends PathNode { - folders?: FolderPathNode[]; -} - /** - * Returns the deepest {@link FolderPathNode} (the target folder). - * Use this for quick access to the leaf folder object from a path response. + * Represents a node in the path tree from the workspace root to the target folder. + * Returned by `getFolderPath`. Use {@link getLeafFolder} to extract the deepest folder object, + * or {@link getLeafFolderPath} to get the full slash-separated path string. + * + * @see getLeafFolder + * @see getLeafFolderPath */ -export function getLeafFolder(node: FolderPathNode): FolderPathNode { - if (node.folders && node.folders.length > 0) { - return getLeafFolder(node.folders[0]); - } - - return node; +export interface FolderPathNode extends PathNode { + folders?: FolderPathNode[]; } /** - * Returns a Unix-like slash-separated path string from the given node to the target folder. - * Use this for quick access to the full path string of the leaf folder from a path response. + * Options for getting the path from the workspace root to the specified folder. * - * @example '/Workspace/Folder/Subfolder' + * @remarks + * It mirrors to the following Smartsheet REST API method: `GET /folders/{folderId}/path` */ -export function getLeafFolderPath(node: FolderPathNode): string { - if (node.folders && node.folders.length > 0) { - return `/${node.name}${getLeafFolderPath(node.folders[0])}`; - } - - return `/${node.name}`; -} - export interface GetFolderPathOptions extends RequestOptions { /** * Folder Id. diff --git a/lib/reports/helpers.ts b/lib/reports/helpers.ts new file mode 100644 index 00000000..c5cc55bc --- /dev/null +++ b/lib/reports/helpers.ts @@ -0,0 +1,54 @@ +import type { PathLeaf } from '../types/PathLeaf'; +import type { ReportPathNode } from './types'; + +/** + * Returns the target {@link PathLeaf} report, or undefined if not reachable. + * Use this for quick access to the leaf report object from a path response. + * + * @param node - The root {@link ReportPathNode} returned by `getReportPath` + * @returns The leaf {@link PathLeaf} report, or `undefined` if not found + * + * @example + * ```typescript + * const path = await client.reports.getReportPath({ reportId: 4583173393803140 }); + * const leaf = getLeafReport(path); + * console.log(leaf?.name); + * ``` + */ +export function getLeafReport(node: ReportPathNode): PathLeaf | undefined { + if (node.reports && node.reports.length > 0) { + return node.reports[0]; + } + + if (node.folders && node.folders.length > 0) { + return getLeafReport(node.folders[0]); + } + + return undefined; +} + +/** + * Returns a Unix-like slash-separated path string from the given node to the target report. + * Use this for quick access to the full path string of the leaf report from a path response. + * + * @param node - The root {@link ReportPathNode} returned by `getReportPath` + * @returns A slash-separated path string, or `undefined` if not reachable + * + * @example + * ```typescript + * const path = await client.reports.getReportPath({ reportId: 4583173393803140 }); + * const pathStr = getLeafReportPath(path); + * console.log(pathStr); // '/Workspace/Folder/Report' + * ``` + */ +export function getLeafReportPath(node: ReportPathNode): string | undefined { + if (node.reports && node.reports.length > 0) { + return `/${node.reports[0].name}`; + } + + if (node.folders && node.folders.length > 0) { + return `/${node.name}${getLeafReportPath(node.folders[0])}`; + } + + return undefined; +} diff --git a/lib/reports/types.ts b/lib/reports/types.ts index 4abee279..69eb1ae7 100644 --- a/lib/reports/types.ts +++ b/lib/reports/types.ts @@ -1665,45 +1665,25 @@ export interface CreateReportResponse extends BaseResponseStatus { // Get Report Path // ============================================================================ +/** + * Represents a node in the path tree from the workspace root to the target report. + * Returned by `getReportPath`. Use {@link getLeafReport} to extract the leaf report object, + * or {@link getLeafReportPath} to get the full slash-separated path string. + * + * @see getLeafReport + * @see getLeafReportPath + */ export interface ReportPathNode extends PathNode { folders?: ReportPathNode[]; reports?: PathLeaf[]; } /** - * Returns the target {@link PathLeaf} report, or undefined if not reachable. - * Use this for quick access to the leaf report object from a path response. - */ -export function getLeafReport(node: ReportPathNode): PathLeaf | undefined { - if (node.reports && node.reports.length > 0) { - return node.reports[0]; - } - - if (node.folders && node.folders.length > 0) { - return getLeafReport(node.folders[0]); - } - - return undefined; -} - -/** - * Returns a Unix-like slash-separated path string from the given node to the target report. - * Use this for quick access to the full path string of the leaf report from a path response. + * Options for getting the path from the workspace root to the specified report. * - * @example '/Workspace/Folder/Report' + * @remarks + * It mirrors to the following Smartsheet REST API method: `GET /reports/{reportId}/path` */ -export function getLeafReportPath(node: ReportPathNode): string | undefined { - if (node.reports && node.reports.length > 0) { - return `/${node.reports[0].name}`; - } - - if (node.folders && node.folders.length > 0) { - return `/${node.name}${getLeafReportPath(node.folders[0])}`; - } - - return undefined; -} - export interface GetReportPathOptions extends RequestOptions { /** * Report Id. diff --git a/lib/sheets/helpers.ts b/lib/sheets/helpers.ts new file mode 100644 index 00000000..0fcc09e5 --- /dev/null +++ b/lib/sheets/helpers.ts @@ -0,0 +1,54 @@ +import type { PathLeaf } from '../types/PathLeaf'; +import type { SheetPathNode } from './types'; + +/** + * Returns the target {@link PathLeaf} sheet, or undefined if not reachable. + * Use this for quick access to the leaf sheet object from a path response. + * + * @param node - The root {@link SheetPathNode} returned by `getSheetPath` + * @returns The leaf {@link PathLeaf} sheet, or `undefined` if not found + * + * @example + * ```typescript + * const path = await client.sheets.getSheetPath({ sheetId: 123456789 }); + * const leaf = getLeafSheet(path); + * console.log(leaf?.name); + * ``` + */ +export function getLeafSheet(node: SheetPathNode): PathLeaf | undefined { + if (node.sheets && node.sheets.length > 0) { + return node.sheets[0]; + } + + if (node.folders && node.folders.length > 0) { + return getLeafSheet(node.folders[0]); + } + + return undefined; +} + +/** + * Returns a Unix-like slash-separated path string from the given node to the target sheet. + * Use this for quick access to the full path string of the leaf sheet from a path response. + * + * @param node - The root {@link SheetPathNode} returned by `getSheetPath` + * @returns A slash-separated path string, or `undefined` if not reachable + * + * @example + * ```typescript + * const path = await client.sheets.getSheetPath({ sheetId: 123456789 }); + * const pathStr = getLeafSheetPath(path); + * console.log(pathStr); // '/Workspace/Folder/Sheet' + * ``` + */ +export function getLeafSheetPath(node: SheetPathNode): string | undefined { + if (node.sheets && node.sheets.length > 0) { + return `/${node.sheets[0].name}`; + } + + if (node.folders && node.folders.length > 0) { + return `/${node.name}${getLeafSheetPath(node.folders[0])}`; + } + + return undefined; +} diff --git a/lib/sheets/types.ts b/lib/sheets/types.ts index e24fd047..a59cbe96 100644 --- a/lib/sheets/types.ts +++ b/lib/sheets/types.ts @@ -5,43 +5,17 @@ import type { PathLeaf } from '../types/PathLeaf'; import type { PathNode } from '../types/PathNode'; -export interface SheetPathNode extends PathNode { - folders?: SheetPathNode[]; - sheets?: PathLeaf[]; -} - -/** - * Returns the target {@link PathLeaf} sheet, or undefined if not reachable. - * Use this for quick access to the leaf sheet object from a path response. - */ -export function getLeafSheet(node: SheetPathNode): PathLeaf | undefined { - if (node.sheets && node.sheets.length > 0) { - return node.sheets[0]; - } - - if (node.folders && node.folders.length > 0) { - return getLeafSheet(node.folders[0]); - } - - return undefined; -} - /** - * Returns a Unix-like slash-separated path string from the given node to the target sheet. - * Use this for quick access to the full path string of the leaf sheet from a path response. + * Represents a node in the path tree from the workspace root to the target sheet. + * Returned by `getSheetPath`. Use {@link getLeafSheet} to extract the leaf sheet object, + * or {@link getLeafSheetPath} to get the full slash-separated path string. * - * @example '/Workspace/Folder/Sheet' + * @see getLeafSheet + * @see getLeafSheetPath */ -export function getLeafSheetPath(node: SheetPathNode): string | undefined { - if (node.sheets && node.sheets.length > 0) { - return `/${node.sheets[0].name}`; - } - - if (node.folders && node.folders.length > 0) { - return `/${node.name}${getLeafSheetPath(node.folders[0])}`; - } - - return undefined; +export interface SheetPathNode extends PathNode { + folders?: SheetPathNode[]; + sheets?: PathLeaf[]; } export interface SheetUserSettings { diff --git a/lib/sights/helpers.ts b/lib/sights/helpers.ts new file mode 100644 index 00000000..9b55b972 --- /dev/null +++ b/lib/sights/helpers.ts @@ -0,0 +1,54 @@ +import type { PathLeaf } from '../types/PathLeaf'; +import type { SightPathNode } from './types'; + +/** + * Returns the target {@link PathLeaf} sight, or undefined if not reachable. + * Use this for quick access to the leaf sight object from a path response. + * + * @param node - The root {@link SightPathNode} returned by `getSightPath` + * @returns The leaf {@link PathLeaf} sight, or `undefined` if not found + * + * @example + * ```typescript + * const path = await client.sights.getSightPath({ sightId: 123456789 }); + * const leaf = getLeafSight(path); + * console.log(leaf?.name); + * ``` + */ +export function getLeafSight(node: SightPathNode): PathLeaf | undefined { + if (node.sights && node.sights.length > 0) { + return node.sights[0]; + } + + if (node.folders && node.folders.length > 0) { + return getLeafSight(node.folders[0]); + } + + return undefined; +} + +/** + * Returns a Unix-like slash-separated path string from the given node to the target sight. + * Use this for quick access to the full path string of the leaf sight from a path response. + * + * @param node - The root {@link SightPathNode} returned by `getSightPath` + * @returns A slash-separated path string, or `undefined` if not reachable + * + * @example + * ```typescript + * const path = await client.sights.getSightPath({ sightId: 123456789 }); + * const pathStr = getLeafSightPath(path); + * console.log(pathStr); // '/Workspace/Folder/Dashboard' + * ``` + */ +export function getLeafSightPath(node: SightPathNode): string | undefined { + if (node.sights && node.sights.length > 0) { + return `/${node.sights[0].name}`; + } + + if (node.folders && node.folders.length > 0) { + return `/${node.name}${getLeafSightPath(node.folders[0])}`; + } + + return undefined; +} diff --git a/lib/sights/types.ts b/lib/sights/types.ts index 808bc78a..d9d467dc 100644 --- a/lib/sights/types.ts +++ b/lib/sights/types.ts @@ -611,45 +611,25 @@ export interface SetSightPublishStatusResponse extends BaseResponseStatus { // Get Sight Path // ============================================================================ +/** + * Represents a node in the path tree from the workspace root to the target sight (dashboard). + * Returned by `getSightPath`. Use {@link getLeafSight} to extract the leaf sight object, + * or {@link getLeafSightPath} to get the full slash-separated path string. + * + * @see getLeafSight + * @see getLeafSightPath + */ export interface SightPathNode extends PathNode { folders?: SightPathNode[]; sights?: PathLeaf[]; } /** - * Returns the target {@link PathLeaf} sight, or undefined if not reachable. - * Use this for quick access to the leaf sight object from a path response. - */ -export function getLeafSight(node: SightPathNode): PathLeaf | undefined { - if (node.sights && node.sights.length > 0) { - return node.sights[0]; - } - - if (node.folders && node.folders.length > 0) { - return getLeafSight(node.folders[0]); - } - - return undefined; -} - -/** - * Returns a Unix-like slash-separated path string from the given node to the target sight. - * Use this for quick access to the full path string of the leaf sight from a path response. + * Options for getting the path from the workspace root to the specified sight (dashboard). * - * @example '/Workspace/Folder/Dashboard' + * @remarks + * It mirrors to the following Smartsheet REST API method: `GET /sights/{sightId}/path` */ -export function getLeafSightPath(node: SightPathNode): string | undefined { - if (node.sights && node.sights.length > 0) { - return `/${node.sights[0].name}`; - } - - if (node.folders && node.folders.length > 0) { - return `/${node.name}${getLeafSightPath(node.folders[0])}`; - } - - return undefined; -} - export interface GetSightPathOptions extends RequestOptions { /** * Sight Id. diff --git a/test/mock-api/folders/get_folder_path.spec.ts b/test/mock-api/folders/get_folder_path.spec.ts index ede26083..f885c86b 100644 --- a/test/mock-api/folders/get_folder_path.spec.ts +++ b/test/mock-api/folders/get_folder_path.spec.ts @@ -12,7 +12,7 @@ import { ERROR_400_MESSAGE, } from './common_test_constants'; import { APIAccessLevel } from '@smartsheet/types'; -import { getLeafFolder, getLeafFolderPath } from '@smartsheet/folders/types'; +import { getLeafFolder, getLeafFolderPath } from '@smartsheet/folders/helpers'; describe('Folders - getFolderPath endpoint tests', () => { const client = createClient(); diff --git a/test/mock-api/reports/get_report_path.spec.ts b/test/mock-api/reports/get_report_path.spec.ts index 31f14a32..86e110e5 100644 --- a/test/mock-api/reports/get_report_path.spec.ts +++ b/test/mock-api/reports/get_report_path.spec.ts @@ -2,7 +2,7 @@ import crypto from 'crypto'; import { createClient, findWireMockRequest } from '../utils/utils'; import { expect } from '@jest/globals'; import { APIAccessLevel } from '@smartsheet/types'; -import { getLeafReport, getLeafReportPath } from '@smartsheet/reports/types'; +import { getLeafReport, getLeafReportPath } from '@smartsheet/reports/helpers'; import { TEST_REPORT_ID, TEST_REPORT_CREATED_AT, diff --git a/test/mock-api/sheets/get_sheet_path.spec.ts b/test/mock-api/sheets/get_sheet_path.spec.ts index dea21fed..507a22a4 100644 --- a/test/mock-api/sheets/get_sheet_path.spec.ts +++ b/test/mock-api/sheets/get_sheet_path.spec.ts @@ -2,7 +2,7 @@ import crypto from 'crypto'; import { createClient, findWireMockRequest } from '../utils/utils'; import { expect } from '@jest/globals'; import { APIAccessLevel } from '@smartsheet/types'; -import { getLeafSheet, getLeafSheetPath } from '@smartsheet/sheets/types'; +import { getLeafSheet, getLeafSheetPath } from '@smartsheet/sheets/helpers'; import { TEST_SHEET_ID, TEST_SHEET_CREATED_AT, diff --git a/test/mock-api/sights/get_sight_path.spec.ts b/test/mock-api/sights/get_sight_path.spec.ts index 2dafd4bf..93abf86b 100644 --- a/test/mock-api/sights/get_sight_path.spec.ts +++ b/test/mock-api/sights/get_sight_path.spec.ts @@ -2,7 +2,7 @@ import crypto from 'crypto'; import { createClient, findWireMockRequest } from '../utils/utils'; import { expect } from '@jest/globals'; import { APIAccessLevel } from '@smartsheet/types'; -import { getLeafSight, getLeafSightPath } from '@smartsheet/sights/types'; +import { getLeafSight, getLeafSightPath } from '@smartsheet/sights/helpers'; import { TEST_SIGHT_ID, TEST_SIGHT_CREATED_AT,