Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- WireMock integration tests for contract testing for POST /2.0/users/{userId}/plans/{planId}/upgrade and POST /2.0/users/{userId}/plans/{planId}/downgrade
- WireMock integration tests for contract testing for DELETE /2.0/users/{userId}/plans/{planId} endpoint
- Remove trailing slashes from routes
- Support for POST /reports/{reportId}/scope endpoint
- Support for DELETE /reports/{reportId}/scope endpoint
### Updated
- listAllUsers url generation
- Folder structure for the Users related WireMock tests
Expand Down
21 changes: 21 additions & 0 deletions lib/reports/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import shareModule from '../share/share';
import type { BaseResponseStatus } from '../types/BaseResponseStatus';
import type { CreateOptions } from '../types/CreateOptions';
import type { RequestCallback } from '../types/RequestCallback';
import type { RequestOptions } from '../types/RequestOptions';
Expand All @@ -16,6 +17,8 @@ import type {
ReportPublish,
SetReportPublishStatusOptions,
SetReportPublishStatusResponse,
AddReportScopeOptions,
RemoveReportScopeOptions,
} from './types';
import * as constants from '../utils/constants';

Expand Down Expand Up @@ -87,6 +90,22 @@ export function create(options: CreateOptions): ReportsApi {
return requestor.put({ ...optionsToSend, ...urlOptions, ...putOptions }, callback);
};

const addReportScope = (
postOptions: AddReportScopeOptions,
callback?: RequestCallback<BaseResponseStatus>
): Promise<BaseResponseStatus> => {
const urlOptions = { url: options.apiUrls.reports + '/' + postOptions.reportId + '/scope' };
return requestor.post({ ...optionsToSend, ...urlOptions, ...postOptions }, callback);
};

const removeReportScope = (
deleteOptions: RemoveReportScopeOptions,
callback?: RequestCallback<BaseResponseStatus>
): Promise<BaseResponseStatus> => {
const urlOptions = { url: options.apiUrls.reports + '/' + deleteOptions.reportId + '/scope' };
return requestor.delete({ ...optionsToSend, ...urlOptions, ...deleteOptions }, callback);
};

return {
listReports,
getReport,
Expand All @@ -95,6 +114,8 @@ export function create(options: CreateOptions): ReportsApi {
getReportAsCSV,
getReportPublishStatus,
setReportPublishStatus,
addReportScope,
removeReportScope,
...shares.create(options),
};
}
125 changes: 125 additions & 0 deletions lib/reports/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,79 @@ export interface ReportsApi {
options: SetReportPublishStatusOptions,
callback?: RequestCallback<SetReportPublishStatusResponse>
) => Promise<SetReportPublishStatusResponse>;

/**
* Add sheets and/or workspaces to a report's scope.
*
* This operation allows you to expand the data sources included in a report by adding
* sheets or workspaces. The report will then include data from these newly added sources.
*
* @param options - {@link AddReportScopeOptions} - Configuration options for the request
* @param callback - {@link RequestCallback}\<{@link BaseResponseStatus}\> - Optional callback function
* @returns Promise\<{@link BaseResponseStatus}\>
*
* @remarks
* **Who can use this operation:**
* - **Permissions:** ADMIN or OWNER access to the report
*
* **Additional notes:**
* - Maximum of 100 scope items can be added at once
* - Requires READ_SHEETS OAuth2 scope or API Token authentication
*
* It mirrors to the following Smartsheet REST API method: `POST /reports/{reportId}/scope`
*
* @example
* ```typescript
* const result = await client.reports.addReportScope({
* reportId: 1234567890,
* body: [
* { assetType: 'SHEET', assetId: 9876543210 },
* { assetType: 'WORKSPACE', assetId: 1122334455 }
* ]
* });
* console.log(result.message); // 'SUCCESS'
* ```
*/
addReportScope: (
options: AddReportScopeOptions,
callback?: RequestCallback<BaseResponseStatus>
) => Promise<BaseResponseStatus>;

/**
* Remove sheets and/or workspaces from a report's scope.
*
* This operation allows you to remove data sources from a report. The report will
* no longer include data from these removed sources.
*
* @param options - {@link RemoveReportScopeOptions} - Configuration options for the request
* @param callback - {@link RequestCallback}\<{@link BaseResponseStatus}\> - Optional callback function
* @returns Promise\<{@link BaseResponseStatus}\>
*
* @remarks
* **Who can use this operation:**
* - **Permissions:** ADMIN or OWNER access to the report
*
* **Additional notes:**
* - Maximum of 100 scope items can be removed at once
* - Requires READ_SHEETS OAuth2 scope or API Token authentication
*
* It mirrors to the following Smartsheet REST API method: `DELETE /reports/{reportId}/scope`
*
* @example
* ```typescript
* const result = await client.reports.removeReportScope({
* reportId: 1234567890,
* body: [
* { assetType: 'SHEET', assetId: 9876543210 }
* ]
* });
* console.log(result.message); // 'SUCCESS'
* ```
*/
removeReportScope: (
options: RemoveReportScopeOptions,
callback?: RequestCallback<BaseResponseStatus>
) => Promise<BaseResponseStatus>;
}

// ============================================================================
Expand Down Expand Up @@ -519,6 +592,30 @@ Only returned in a response if readOnlyFullEnabled = true.
readOnlyFullShowToolbar?: boolean;
}

/**
* Asset types that can be included in a report's scope.
*/
export enum ReportAssetType {
SHEET = 'SHEET',
WORKSPACE = 'WORKSPACE',
}

/**
* Represents an asset (sheet or workspace) in a report's scope.
*/
export interface ReportScopeAsset {
/**
* The asset type to be included in the scope of the report.
* @see ReportAssetType
*/
assetType: ReportAssetType;

/**
* The ID of the asset according to its assetType.
*/
assetId: number;
}

// ============================================================================
// List Reports
// ============================================================================
Expand Down Expand Up @@ -722,3 +819,31 @@ export interface SetReportPublishStatusResponse extends BaseResponseStatus {
failedItems?: FailedItem[];
result: ReportPublish;
}

// ============================================================================
// Add Report Scope
// ============================================================================

/**
* Options for adding report scope.
*/
export interface AddReportScopeOptions extends RequestOptions<never, ReportScopeAsset[]> {
/**
* Report ID.
*/
reportId: number;
}

// ============================================================================
// Remove Report Scope
// ============================================================================

/**
* Options for removing report scope.
*/
export interface RemoveReportScopeOptions extends RequestOptions<never, ReportScopeAsset[]> {
/**
* Report ID.
*/
reportId: number;
}
7 changes: 6 additions & 1 deletion lib/utils/httpRequestor.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export function create(requestorConfig) {

const postFile = (options, callback) => methodRequest(options, request.post, 'POST', callback, getFileBody(options));

const deleteFunc = (options, callback) => methodRequest(options, request.delete, 'DELETE', callback);
const deleteFunc = (options, callback) => methodRequest(options, request.delete, 'DELETE', callback, options.body);

const put = (options, callback) => methodRequest(options, request.put, 'PUT', callback, options.body);

Expand Down Expand Up @@ -150,6 +150,11 @@ export function create(requestorConfig) {
return method(url, body, requestOptions);
}

if (methodName === 'DELETE' && body) {
// For DELETE requests with a body, axios requires the body to be in config.data
return method(url, { ...requestOptions, data: body });
}

return method(url, requestOptions);
};

Expand Down
10 changes: 9 additions & 1 deletion test/functional/client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ describe('Client Unit Tests', () => {
describe('#reports', () => {
it('should have reports object', () => {
expect(smartsheet).toHaveProperty('reports');
expect(Object.keys(smartsheet.reports)).toHaveLength(12);
expect(Object.keys(smartsheet.reports)).toHaveLength(14);
});

it('should have get methods', () => {
Expand All @@ -191,10 +191,18 @@ describe('Client Unit Tests', () => {
expect(smartsheet.reports).toHaveProperty('getReportPublishStatus');
});

it('should have create methods', () => {
expect(smartsheet.reports).toHaveProperty('addReportScope');
});

it('should have update methods', () => {
expect(smartsheet.reports).toHaveProperty('setReportPublishStatus');
expect(smartsheet.reports).toHaveProperty('sendReportViaEmail');
});

it('should have delete methods', () => {
expect(smartsheet.reports).toHaveProperty('removeReportScope');
});
});

describe('#server', () => {
Expand Down
111 changes: 111 additions & 0 deletions test/mock-api/reports/add_report_scope.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import crypto from 'crypto';
import { createClient, findWireMockRequest } from '../utils/utils';
import { expect } from '@jest/globals';
import {
TEST_REPORT_ID,
TEST_SHEET_ID,
TEST_WORKSPACE_ID,
TEST_ASSET_TYPE_SHEET,
TEST_ASSET_TYPE_WORKSPACE,
TEST_SUCCESS_MESSAGE,
TEST_SUCCESS_RESULT_CODE,
ERROR_500_STATUS_CODE,
ERROR_500_MESSAGE,
ERROR_400_STATUS_CODE,
ERROR_400_MESSAGE
} from './common_test_constants';
import { ReportAssetType } from '@smartsheet/reports/types';

describe('Reports - addReportScope endpoint tests', () => {
const client = createClient();

it('addReportScope generated url is correct', async () => {
const requestId = crypto.randomUUID();
const options = {
reportId: TEST_REPORT_ID,
body: [
{ assetType: ReportAssetType.SHEET, assetId: TEST_SHEET_ID }
],
customProperties: {
'x-request-id': requestId,
'x-test-name': '/reports/add-report-scope/all-response-body-properties'
}
};
await client.reports.addReportScope(options);
const matchedRequest = await findWireMockRequest(requestId);

const parsedUrl = new URL(matchedRequest.absoluteUrl);
expect(parsedUrl.pathname).toEqual(`/2.0/reports/${TEST_REPORT_ID}/scope`);
});

it('addReportScope all response body properties', async () => {
const requestId = crypto.randomUUID();
const testBody = [
{ assetType: ReportAssetType.SHEET, assetId: TEST_SHEET_ID },
{ assetType: ReportAssetType.WORKSPACE, assetId: TEST_WORKSPACE_ID }
];
const options = {
reportId: TEST_REPORT_ID,
body: testBody,
customProperties: {
'x-request-id': requestId,
'x-test-name': '/reports/add-report-scope/all-response-body-properties'
}
};
const response = await client.reports.addReportScope(options);
const matchedRequest = await findWireMockRequest(requestId);

expect(response).toEqual({
message: TEST_SUCCESS_MESSAGE,
resultCode: TEST_SUCCESS_RESULT_CODE
});

const body = JSON.parse(matchedRequest.body);
expect(body).toEqual([
{ assetType: TEST_ASSET_TYPE_SHEET, assetId: TEST_SHEET_ID },
{ assetType: TEST_ASSET_TYPE_WORKSPACE, assetId: TEST_WORKSPACE_ID }
]);
});

it('addReportScope error 500 response', async () => {
const requestId = crypto.randomUUID();
const options = {
reportId: TEST_REPORT_ID,
body: [
{ assetType: ReportAssetType.SHEET, assetId: TEST_SHEET_ID }
],
customProperties: {
'x-request-id': requestId,
'x-test-name': '/errors/500-response'
}
};
try {
await client.reports.addReportScope(options);
expect(true).toBe(false); // Expected an error to be thrown
} catch (error) {
expect(error.statusCode).toBe(ERROR_500_STATUS_CODE);
expect(error.message).toBe(ERROR_500_MESSAGE);
}
});

it('addReportScope error 400 response', async () => {
const requestId = crypto.randomUUID();
const options = {
reportId: TEST_REPORT_ID,
body: [
{ assetType: ReportAssetType.SHEET, assetId: TEST_SHEET_ID }
],
customProperties: {
'x-request-id': requestId,
'x-test-name': '/errors/400-response'
}
};
try {
await client.reports.addReportScope(options);
expect(true).toBe(false); // Expected an error to be thrown
} catch (error) {
expect(error.statusCode).toBe(ERROR_400_STATUS_CODE);
expect(error.message).toBe(ERROR_400_MESSAGE);
}
});
});
6 changes: 6 additions & 0 deletions test/mock-api/reports/common_test_constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ export const TEST_SOURCE_WORKSPACE_NAME = 'Source Workspace';
export const TEST_SOURCE_WORKSPACE_ACCESS_LEVEL = 'VIEWER';
export const TEST_SOURCE_WORKSPACE_PERMALINK = 'https://app.smartsheet.com/workspaces/source-workspace';

// Report Scope Assets
export const TEST_SHEET_ID = 9876543210;
export const TEST_WORKSPACE_ID = 1122334455;
export const TEST_ASSET_TYPE_SHEET = 'SHEET';
export const TEST_ASSET_TYPE_WORKSPACE = 'WORKSPACE';

// Report Columns (Column type requires all these properties)
export const TEST_COLUMN_1_ID = 1111111111111111;
export const TEST_COLUMN_1_INDEX = 0;
Expand Down
Loading
Loading